<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
  "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
  [
    <!ENTITY selfURL
        "http://www.nmt.edu/tcc/help/lang/python/examples/sidereal/ims/">
    <!ENTITY specURL
        "http://www.nmt.edu/tcc/help/lang/python/examples/sidereal/">
    <!ENTITY py    "<filename>sidereal.py</filename>">
    <!ENTITY spy   "sidereal.py">
  ]
>
<article>
  <articleinfo>
    <title>&py;: Internal maintenance specification</title>
    <titleabbrev>&py;: Internal specification</titleabbrev>
    <authorgroup>
      <author>
        <firstname>John W.</firstname>
        <surname>Shipman</surname>
      </author>
    </authorgroup>
    <address><email>tcc-doc@nmt.edu</email>
    </address>
    <revhistory>
      <revision>
        <revnumber>$Revision: 1.43 $</revnumber>
        <date>$Date: 2008/01/01 01:55:56 $</date>
      </revision>
    </revhistory>
  </articleinfo>
  <abstract>
    <para>
      &py; is a Python module to perform certain astronomical
      calculations.  This document contains and describes the
      implementation of the module.
    </para>
    <para>
      This publication is available in <ulink url="&selfURL;/"
      >Web form</ulink > and also as a <ulink
      url="&selfURL;siderealims.pdf" >PDF document</ulink >.
      Please forward any comments to <userinput
      >tcc-doc@nmt.edu</userinput >.
    </para>
  </abstract>
  <section id='intro'>
    <title>Introduction</title>
    <para>
      This document contains the source code for the &py;
      module.  The technique of embedding the code in a document
      that explains its function is called <firstterm >literate
      programming</firstterm >; see <ulink
      url='http://www.nmt.edu/~shipman/soft/litprog/' >the
      author's Lightweight Literate Programming page</ulink >.
    </para>
    <para>
      The reader is assumed to be familiar with the Python
      programming language, including object-oriented
      programming.  Some familiarity with the problem
      domain&#x2014;spherical astronomy&#x2014;will also be
      useful.
    </para>
    <section id='references'>
      <title>References</title>
      <para>
        The primary reference is Peter Duffett-Smith's <citetitle
        >Practical Astronomy with Your Calculator</citetitle >.
        For a full citation, see <ulink
url='http://www.nmt.edu/tcc/help/lang/python/examples/sidereal/index.html#introduction'
        >the specification</ulink >.
      </para>
      <para>
        Other useful online references:
      </para>
      <itemizedlist>
        <listitem>
          <para>
            Documentation for Python's <ulink
            url='http://docs.python.org/lib/module-math.html'
            ><code >math</code > module</ulink >.
          </para>
        </listitem>
        <listitem>
          <para>
            Documentation for Python's <ulink
            url='http://docs.python.org/lib/module-datetime.html'
            ><code >datetime</code > module</ulink >.
          </para>
        </listitem>
      </itemizedlist>
    </section> <!--references-->
    <section id='files'>
      <title>Online files</title>
      <para>
        Relevant online files:
      </para>
      <itemizedlist>
        <listitem>
          <para>
            <ulink url='&selfURL;&spy;' >&py;</ulink >: The Python
            source for the module.
          </para>
        </listitem>
        <listitem>
          <para>
            <ulink url='&selfURL;siderealims.xml' ><filename
            >siderealims.xml</filename ></ulink >: The DocBook
            source for this document.
          </para>
        </listitem>
      </itemizedlist>
    </section> <!--files-->
  </section> <!--intro-->
  <section id='jd'>
    <title><code >jd</code >: Convert date and time to Julian
    date</title>
    <para>
      Most of the work in this standalone script is checking and
      converting the command line arguments.  We'll make heavy
      use of Python's <code >re</code > regular expression
      package.
    </para>
    <section id='jd-prologue'>
      <title>Prologue</title>
      <programlisting role='outFile:jd'
>#!/usr/bin/env python
#================================================================
# jd:  Convert date and time to Julian date
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------
</programlisting>
    </section> <!--jd-prologue-->
    <section id='jd-imports'>
      <title>Imports</title>
      <para>
        We'll need the standard Python <code >sys</code > module
        to get at command line arguments and I/O streams.
      </para>
      <programlisting role='outFile:jd'
>#================================================================
# Imports
#----------------------------------------------------------------

import sys
</programlisting>
      <para>
        In addition to the &py; module, we'll need Python's <code
        >datetime</code > package required by the <code
        >JulianDate</code > class.
      </para>
      <programlisting role='outFile:jd'
>import sidereal
import datetime
</programlisting>
    </section> <!--jd-imports-->
    <section id='jd-main'>
      <title><code >main()</code ></title>
      <para>
        The main program first processes its command line
        arguments.  Then, if all is well, it converts them to a
        <code >JulianDate</code > and displays that as a <code
        >float</code >.
      </para>
      <programlisting role='outFile:jd'
># - - -   m a i n

def main():
    """jd main program.
    """

    #-- 1 --
    # [ if the arguments in sys.argv are valid ->
    #     dt  :=  a datetime.datetime instance representing the
    #             date and time expressed in those arguments ]
    dt  =  argCheck()

    #-- 2 --
    # [ jd  :=  a JulianDate instance representing dt ]
    jd  =  sidereal.JulianDate.fromDatetime ( dt )

    #-- 3 --
    print float(jd)
</programlisting>
    </section> <!--jd-main-->
    <section id='jd-argCheck'>
      <title><code >argCheck()</code >: Check command line
      arguments</title>
      <para>
        This function does all checking of command line
        arguments, and boils them down to a <code
        >datetime.datetime</code > instance.
      </para>
      <programlisting role='outFile:jd'
># - - -   a r g C h e c k

def  argCheck():
    """Check and convert the command line argument(s).
    """
</programlisting>
      <para>
        The command line arguments in <code >sys.argv[1:]</code >
        can take either of two forms:
      </para>
      <itemizedlist spacing="compact">
        <listitem>
          <para>
            A single argument, consisting of an ISO standard
            date, optionally followed by letter &#x201c;<code
            >T</code >&#x201d; (in either case) and a time, which
            may omit minutes and seconds; or,
          </para>
        </listitem>
        <listitem>
          <para>
            Two arguments: the first is an ISO date, and the
            second is the time in the same form as part after the
            &#x201c;<code >T</code >&#x201d; in the
            single-argument case.
          </para>
        </listitem>
      </itemizedlist>
      <para>
        In the first case, we use <xref linkend='parseDatetime'
        /> to parse the single argument; in the second, we use
        <xref linkend='parseDate' /> and <xref
        linkend='parseTime' /> and then combine them into a <code
        >datetime.datetime</code > instance.
      </para>
      <programlisting role='outFile:jd'
>    #-- 1 --
    # [ argList  :=  the command line arguments ]
    argList  =  sys.argv[1:]

    #-- 2 --
    # [ if (len(argList)==1) and argList[0] is a valid
    #   date-time string ->
    #     dt  :=  that date-time as a datetime.datetime instance
    #   else if (len(argList)==2) and (argList[0] is a valid
    #   date) and (argList[1] is a valid time) ->
    #     dt  :=  a datetime.datetime representing that date
    #             and time
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    if  len(argList) == 1:
        try:
            dt  =  sidereal.parseDatetime ( argList[0] )
        except SyntaxError, detail:
            usage ( "Invalid date-time: %s" % detail )
    elif  len(argList) == 2:
        try:
            date  =  sidereal.parseDate ( argList[0] )
        except SyntaxError, detail:
            usage ( "Invalid date: %s" % detail )
        try:
            time  =  sidereal.parseTime ( argList[1] )
        except SyntaxError, detail:
            usage ( "Invalid time: %s" % detail )
        dt  =  date.combine ( date, time )
    else:
        usage ( "Incorrect number of arguments." )

    #-- 3 --
    return dt
</programlisting>
    </section> <!--jd-argCheck-->
    <section id='jd-usage'>
      <title><code >usage()</code >: Print a usage message and stop</title>
      <programlisting role='outFile:jd'
># - - -   u s a g e

def usage ( *L ):
    """Print a usage message and stop.

      [ L is a list of strings ->
          sys.stderr  +:=  (usage message) + (elements of L,
                           concatenated)
          stop execution ]
    """
    print >>sys.stderr, "*** Usage:"
    print >>sys.stderr, "***   jd yyyy-mm-dd[Thh[:mm[:ss]]]"
    print >>sys.stderr, "*** or:"
    print >>sys.stderr, "***   jd yyyy-mm-dd hh[:mm[:ss]]"
    print >>sys.stderr, "*** Error: %s" % "".join(L)
    raise SystemExit
</programlisting>
    </section> <!--jd-usage-->
    <section id='jd-epilogue'>
      <title>Epilogue</title>
      <programlisting role='outFile:jd'
>#================================================================
# Epilogue
#----------------------------------------------------------------

if __name__ == "__main__":
    main()
</programlisting>
    </section> <!--jd-epilogue-->
  </section> <!--jd-->
  <section id='conjd'>
    <title><code >conjd</code >: Convert Julian date to date and
    time</title>
    <para>
      This script takes one command line argument, a Julian date
      as a float, and converts it to civil units.
    </para>
    <section id='conjd-prologue'>
      <title>Prologue</title>
      <programlisting role='outFile:conjd'
>#!/usr/bin/env python
#================================================================
# conjd:  Convert Julian date to date and time
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------
</programlisting>
    </section> <!--conjd-prologue-->
    <section id='conjd-imports'>
      <title>Imports</title>
      <para>
        Modules needed include the standard <code >sys</code >
        module and of course &py;.
      </para>
      <programlisting role='outFile:conjd'
>#================================================================
# Imports
#----------------------------------------------------------------

import sys
from sidereal import *
</programlisting>
    </section> <!--conjd-imports-->
    <section id='conjd-main'>
      <title><code >main()</code ></title>
      <para>
        There are only two possible error conditions: wrong
        number of command line arguments; the argument is not a
        float.
      </para>
      <programlisting role='outFile:conjd'
># - - -   m a i n

def main():
    """conjd main program.
    """

    #-- 1 --
    # [ if  sys.argv[1:] is a single float ->
    #     j  :=  that float
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    argList  =  sys.argv[1:]
    if  len(argList) != 1:
        usage ( "Wrong argument count." )
    else:
        try:
            j  =  float ( argList[0] )
        except ValueError, detail:
            usage ( "Invalid argument: %s" % detail )
</programlisting>
      <para>
        First we make <code >j</code > into a <code
        >JulianDate</code > instance.  Then we use the <code
        >.datetime()</code > method on that instance to get a
        regular <code >datetime.datetime</code > instance.  Final
        output is produced by applying <code >str()</code > to
        the <code >datetime</code >, which produces an ISO date.
      </para>
      <programlisting role='outFile:conjd'
>    #-- 2 --
    # [ jd  :=  a JulianDate instance for Julian date j ]
    jd  =  JulianDate ( j )

    #-- 3 --
    # [ dt  :=  jd as a datetime.datetime instance ]
    dt  =  jd.datetime()

    #-- 4 --
    # [ sys.stdout  +:=  dt in ISO form ]
    print str(dt)
</programlisting>
    </section> <!--conjd-main-->
    <section id='conjd-usage'>
      <title><code >usage()</code >: Write an error message and
      terminate</title>
      <programlisting role='outFile:conjd'
># - - -   u s a g e

def usage ( *L ):
    """Write a usage message and stop.

      [ L is a list of strings ->
          sys.stderr  +:=  (usage message) + (joined elements of L)
          stop execution ]
    """
    print >>sys.stderr, "*** Usage:"
    print >>sys.stderr, "***   conjd NNNNNNN.NN..."
    print >>sys.stderr, "*** where NNNNNNN.NN is the Julian date."
    print >>sys.stderr, "*** Error: %s" % "".join(L)
    raise SystemExit
</programlisting>
    </section> <!--conjd-usage-->
    <section id='conjd-epilogue'>
      <title>Epilogue</title>
      <programlisting role='outFile:conjd'
>#================================================================
# Epilogue
#----------------------------------------------------------------

if __name__ == "__main__":
    main()
</programlisting>
    </section> <!--conjd-epilogue-->
  </section> <!--conjd-->
  <section id='rdaa'>
    <title><code >rdaa</code >: Equatorial to horizon
    coordinates</title>
    <para>
      For a given celestial position in equatorial coordinates,
      and a given observer's place and time, this script reports
      the position in horizon coordinates.
    </para>
    <section id='rdaa-prologue'>
      <title>Prologue</title>
      <programlisting role='outFile:rdaa'
>#!/usr/bin/env python
#================================================================
# rdaa: Convert right ascension/declination to azimuth/altitude
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------
</programlisting>
    </section> <!--rdaa-prologue-->
    <section id='rdaa-imports'>
      <title>Imports</title>
      <para>
        We'll need the usual <code >sys</code > module for the
        command line arguments and I/O streams, and the regular
        expression module <code >re</code > for some parsing.
      </para>
      <programlisting role='outFile:rdaa'
>#================================================================
# Imports
#----------------------------------------------------------------
import sys, re
</programlisting>
      <para>
        We also need the <code >sidereal</code > package.
      </para>
      <programlisting role='outFile:rdaa'
>import sidereal
</programlisting>
    </section> <!--rdaa-imports-->
    <section id='rdaa-constants'>
      <title>Constants</title>
      <para>
        We'll need a pre-compiled regular expression to search
        for the plus or minus sign that separates the right
        ascension and declination on the command line.  The
        &#x201c;<code >-</code >&#x201d; is escaped because
        otherwise it has special meaning inside &#x201c;<code
        >[...]</code >&#x201d; regular expressions.
      </para>
      <programlisting role='outFile:rdaa'
>#================================================================
# Manifest consants
#----------------------------------------------------------------

SIGN_PAT  =  re.compile ( r'[\-+]' )
</programlisting>
    </section> <!--rdaa-constants-->
    <section id='rdaa-main'>
      <title><code >main()</code ></title>
      <para>
        The main sequence comes down to three steps: check and
        convert the command line arguments; find the local
        sidereal time; and then find the horizon coordinates.
      </para>
      <programlisting role='outFile:rdaa'
># - - - - -   m a i n

def main():
    """Main program for rdaa.
    """

    #-- 1 --
    # [ if sys.argv contains a valid set of command line
    #   arguments ->
    #     raDec  :=  the right ascension and declination as
    #                a sidereal.RADec instance
    #     latLon  :=  the observer's location as a
    #                 sidereal.LatLon instance
    #     dt  :=  the observer's date and time as a
    #             datetime.datetime instance
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    raDec, latLon, dt  =  checkArgs()
</programlisting>
      <para>
        Since the time specified might have a time zone attached,
        we'll need to convert it to UTC.
      </para>
      <programlisting role='outFile:rdaa'
>    #-- 2 --
    # [ if dt has no time zone information ->
    #     utc  :=  dt
    #   else ->
    #     utc  :=  the UTC equivalent to dt ]
    if  ( (dt.tzinfo is None) or
          (dt.utcoffset() is None) ):
        utc  =  dt
    else:
        utc  =  dt - dt.utcoffset()
</programlisting>
      <para>
        Just for convenience and user reference, we'll compute
        and display the observer's local sidereal time.
      </para>
      <programlisting role='outFile:rdaa'
>    #-- 3 --
    # [ sys.stdout  +:=  local sidereal time for dt and latLon ]
    gst  =  sidereal.SiderealTime.fromDatetime ( utc )
    lst  =  gst.lst ( latLon.lon )
    print "Equatorial coordinates:", raDec
    print "Observer's location:", latLon
    print "Observer's time:", dt
    print "Local sidereal time is", lst
</programlisting>
      <para>
        Now find the hour angle, convert to horizon coordinates,
        and print the result.
      </para>
      <programlisting role='outFile:rdaa'
>    #-- 4 --
    # [ h  :=  hour angle for raDec at time (utc) and longitude
    #          (latLon.lon) ]
    h  =  raDec.hourAngle ( utc, latLon.lon )

    #-- 5 --
    # [ aa  :=  horizon coordinates of raDec at hour angle h
    #           as a sidereal.AltAz instance ]
    aa  =  raDec.altAz ( h, latLon.lat )

    #-- 6 --
    print "Horizon coordinates:", aa
</programlisting>
    </section> <!--rdaa-main-->
    <section id='rdaa-checkArgs'>
      <title><code >checkArgs()</code >: Process command-line
      arguments</title>
      <programlisting role='outFile:rdaa'
># - - -   c h e c k A r g s

def checkArgs():
    """Process all command line arguments.

      [ if sys.argv[1:] is a valid set of command line arguments ->
          return (raDec, latLon, dt) where raDec is a set of
          celestial coordinates as a sidereal.RADec instance,
          latLon is position as a sidereal.LatLon instance, and
          dt is a datetime.datetime instance
        else ->
          sys.stderr  +:=  error message
          stop execution ]
    """
</programlisting>
      <para>
        There should be exactly four arguments:
      </para>
      <itemizedlist spacing="compact">
        <listitem>
          <para>
            The combined right ascension and declination,
            separated by the mandatory &#x201c;<code >+</code
            >&#x201d; or &#x201c;<code >-</code >&#x201d;.
          </para>
        </listitem>
        <listitem>
          <para>
            The latitude, as an angle followed by &#x201c;<code
            >n</code >&#x201d; or &#x201c;<code >s</code
            >&#x201d; (case-insensitive).
          </para>
        </listitem>
        <listitem>
          <para>
            The longitude, as an angle followed by &#x201c;<code
            >e</code >&#x201d; or &#x201c;<code >w</code
            >&#x201d; (case-insensitive).
          </para>
        </listitem>
        <listitem>
          <para>
            The timestamp, as a combined date-time string.
          </para>
        </listitem>
      </itemizedlist>
      <programlisting role='outFile:rdaa'
>    #-- 1 --
    # [ if sys.argv[1:] has exactly four elements ->
    #     rawRADec, rawLat, rawLon, rawDT  :=  those elements
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    argList  =  sys.argv[1:]
    if  len(argList) != 4:
        usage ("Incorrect command line argument count." )
    else:
        rawRADec, rawLat, rawLon, rawDT  =  argList
</programlisting>
      <para>
        The work of checking the first argument is delegated to
        subsidiary routines: <xref linkend='rdaa-checkRADec' />.
        For the rest, standard parsing functions exist:
        <xref linkend='parseLat' />, <xref
        linkend='parseLon' />, and <xref linkend='parseDatetime' />.
      </para>
      <programlisting role='outFile:rdaa'
>    #-- 2 --
    # [ if rawRADec is a valid set of equatorial coordinates ->
    #     raDec  :=  those coordinates as a sidereal.RADec instance
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    raDec  =  checkRADec ( rawRADec )

    #-- 3 --
    # [ if rawLat is a valid latitude ->
    #     lat  :=  that latitude in radians
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        lat  =  sidereal.parseLat ( rawLat )
    except SyntaxError, detail:
        usage ( "Invalid latitude: %s" % detail )

    #-- 4 --
    # [ if rawLon is a valid longitude ->
    #     lon  :=  that longitude in radians
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        lon  =  sidereal.parseLon ( rawLon )
    except SyntaxError, detail:
        usage ( "Invalid longitude: %s" % detail )

    #-- 5 --
    # [ if rawDT is a valid date-time string ->
    #     dt  :=  that date-time as a datetime.datetime instance
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        dt  =  sidereal.parseDatetime ( rawDT )
    except SyntaxError, detail:
        usage ( "Invalid timestamp: %s" % detail )

    #-- 6 --
    latLon  =  sidereal.LatLon ( lat, lon )
    return  (raDec, latLon, dt)
</programlisting>
    </section> <!--rdaa-checkArgs-->
    <section id='rdaa-usage'>
      <title><code >usage()</code >: Print a message and stop</title>
      <programlisting role='outFile:rdaa'
># - - -   u s a g e

def usage ( *L ):
    """Print a usage message and stop.

      [ L is a list of strings ->
          sys.stderr  +:=  (usage message) + (elements of L,
                           concatenated)
          stop execution ]
    """
    print >>sys.stderr, "*** Usage:"
    print >>sys.stderr, "***   rdaa RA+dec lat lon datetime"
    print >>sys.stderr, "*** Or:"
    print >>sys.stderr, "***   rdaa RA-dec lat lon datetime"
    print >>sys.stderr, "*** Error: %s" % "".join(L)
    raise SystemExit
</programlisting>
    </section> <!--rdaa-usage-->
    <section id='rdaa-checkRADec'>
      <title><code >checkRADec</code >: Check equatorial
      coordinates</title>
      <programlisting role='outFile:rdaa'
># - - -   c h e c k R A D e c

def checkRADec ( rawRADec ):
    """Check and convert a pair of equatorial coordinates.

      [ rawRADec is a string ->
          if rawRADec is a valid set of equatorial coordinates ->
            return those coordinates as a sidereal.RADec instance
          else ->
            sys.stderr  +:=  error message
            stop execution ]
    """
</programlisting>
      <para>
        A set of equatorial coordinates consists of a quantity in
        hours, a &#x201c;<code >+</code >&#x201d; or &#x201c;<code
        >-</code >&#x201d; sign, and a declination as an angle.  So
        we'll start by finding and saving the sign and breaking the
        string into its two parts.  The <code >.search()</code > method
        of a precompiled regular expression searches for that pattern
        anywhere in a string.  For <code >SIGN_PAT</code >, see <xref
        linkend='rdaa-constants' />.
      </para>
      <programlisting role='outFile:rdaa'
>    #-- 1 --
    # [ if rawRADec contains either a '+' or a '-' ->
    #     m  :=  a re.match instance describing the first matching
    #            character
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    m  =  SIGN_PAT.search ( rawRADec )
    if  m is None:
        usage ( "Equatorial coordinates must be separated by "
                "'+' or '-'." )
    #-- 2 --
    # [ rawRA  :=  rawRADec up to the match described by m
    #   sign  :=  characters matched by m
    #   rawDec  :=  rawRADec past the match described by m ]
    rawRA  =  rawRADec[:m.start()]
    sign  =  m.group()
    rawDec  =  rawRADec[m.end():]
</programlisting>
      <para>
        To convert the raw RA, we'll use <xref linkend='parseHours' />.
      </para>
      <programlisting role='outFile:rdaa'
>    #-- 3 --
    # [ if rawRA is a valid hours expression ->
    #     ra  :=  rawRA as radians
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        raHours  =  sidereal.parseHours ( rawRA )
        ra  =  sidereal.hoursToRadians ( raHours )
    except SyntaxError, detail:
        usage ( "Right ascension '%s' should have the form "
                "'NNh[NNm[NN.NNNs]]'." % rawRA )
</programlisting>
      <para>
        To convert the raw declination, we'll use <xref
        linkend='parseAngle' />.
      </para>
      <programlisting role='outFile:rdaa'
>    #-- 4 --
    # [ if rawDec is a valid angle expression ->
    #     absDec  :=  that angle in radians
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        absDec  =  sidereal.parseAngle ( rawDec )
    except SyntaxError, detail:
        usage ( "Right ascension '%s' should have the form "
                "'NNd[NNm[NN.NNNs]]'." % rawDec )
</programlisting>
      <para>
        All that remains is to attach the sign to the declination, wrap
        both coordinates in an <code >RADec</code > instance, and
        return it.
      </para>
      <programlisting role='outFile:rdaa'
>    #-- 5 --
    if  sign == '-':   dec  =  - absDec
    else:              dec  =  absDec

    #-- 6 --
    return sidereal.RADec ( ra, dec )
</programlisting>
    </section> <!--rdaa-checkRADec-->
    <section id='rdaa-epilogue'>
      <title>Epilogue</title>
      <programlisting role='outFile:rdaa'
>#================================================================
# Epilogue
#----------------------------------------------------------------

if  __name__ == "__main__":
    main()
</programlisting>
    </section> <!--rdaa-epilogue-->
  </section> <!--rdaa-->
  <section id='aard'>
    <title><code >aard</code >: Horizon to celestial
    coordinates</title>
    <para>
      This script is structurally nearly identical to <xref
      linkend='rdaa' />, so the comments will be fairly skeletal
      here except in spots where the scripts differ.
    </para>
    <section id='aard-prologue'>
      <title>Prologue</title>
      <programlisting role='outFile:aard'
>#!/usr/bin/env python
#================================================================
# aard: Convert azimuth/altitude to right ascension/declination
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------
</programlisting>
    </section> <!--aard-prologue-->
    <section id='aard-imports'>
      <title>Imports</title>
      <programlisting role='outFile:aard'
>#================================================================
# Imports
#----------------------------------------------------------------
import sys, re
import sidereal
</programlisting>
    </section> <!--aard-imports-->
    <section id='aard-constants'>
      <title>Constants</title>
      <programlisting role='outFile:aard'
>#================================================================
# Manifest consants
#----------------------------------------------------------------

SIGN_PAT  =  re.compile ( r'[\-+]' )
</programlisting>
    </section> <!--aard-constants-->
    <section id='aard-main'>
      <title><code >main()</code ></title>
      <para>
        As with <xref linkend='rdaa-main' />, first we check out
        the command line arguments, then convert from horizon to
        celestial coordinates and print the result.
      </para>
      <programlisting role='outFile:aard'
># - - - - -   m a i n

def main():
    """Main program for aard.
    """

    #-- 1 --
    # [ if sys.argv contains a valid set of command line
    #   arguments ->
    #     altAz  :=  the azimuth and altitude as
    #                a sidereal.AltAz instance
    #     latLon  :=  the observer's location as a
    #                 sidereal.LatLon instance
    #     dt  :=  the observer's date and time as a
    #             datetime.datetime instance
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    altAz, latLon, dt  =  checkArgs()

    #-- 2 --
    # [ if dt has no time zone information ->
    #     utc  :=  dt
    #   else ->
    #     utc  :=  the UTC equivalent to dt ]
    if  ( (dt.tzinfo is None) or
          (dt.utcoffset() is None) ):
        utc  =  dt
    else:
        utc  =  dt - dt.utcoffset()
</programlisting>
      <para>
        Display the local sidereal time in case the observer is
        interested.
      </para>
      <programlisting role='outFile:aard'
>    #-- 3 --
    # [ sys.stdout  +:=  local sidereal time for dt and latLon ]
    gst  =  sidereal.SiderealTime.fromDatetime ( utc )
    lst  =  gst.lst ( latLon.lon )
    print "Horizon coordinates:", altAz
    print "Observer's location:", latLon
    print "Observer's time:", dt
    print "Local sidereal time is", lst
</programlisting>
      <para>
        Here is where this script diverges from <xref
        linkend='rdaa-main' />.  To convert from horizon to
        equatorial coordinates, you need a local sidereal time
        and an observer's location.
      </para>
      <programlisting role='outFile:aard'
>    #-- 4 --
    # [ raDec  :=  equatorial coordinates of self for local
    #       sidereal time (lst) and location (latLon) ]
    raDec  =  altAz.raDec ( lst, latLon )

    #-- 5 --
    print "Equatorial coordinates:", raDec
</programlisting>
    </section> <!--aard-main-->
    <section id='aard-checkArgs'>
      <title><code >checkArgs()</code >: Process command line
      arguments</title>
      <para>
        This is pretty similar to <xref linkend='rdaa-checkArgs'
        />, except that the first item is an azimuth, sign, and
        altitude.
      </para>
      <programlisting role='outFile:aard'
># - - -   c h e c k A r g s

def checkArgs():
    """Process all command line arguments.

      [ if sys.argv[1:] is a valid set of command line arguments ->
          return (altAz, latLon, dt) where altAz is a set of
          horizon coordinates as a sidereal.AltAz instance,
          latLon is position as a sidereal.LatLon instance, and
          dt is a datetime.datetime instance
        else ->
          sys.stderr  +:=  error message
          stop execution ]
    """

    #-- 1 --
    # [ if sys.argv[1:] has exactly four elements ->
    #     rawAltAz, rawLat, rawLon, rawDT  :=  those elements
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    argList  =  sys.argv[1:]
    if  len(argList) != 4:
        usage ("Incorrect command line argument count." )
    else:
        rawAltAz, rawLat, rawLon, rawDT  =  argList
</programlisting>
      <para>
        For the checking of the first argument, see <xref
        linkend='aard-checkAltAz' />.  The rest are as in <xref
        linkend='rdaa-checkArgs' />.
      </para>
      <programlisting role='outFile:aard'
>    #-- 2 --
    # [ if rawAltAz is a valid set of horizon coordinates ->
    #     altAz  :=  those coordinates as a sidereal.AltAz instance
    altAz  =  checkAltAz ( rawAltAz )

    #-- 3 --
    # [ if rawLat is a valid latitude ->
    #     lat  :=  that latitude in radians
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        lat  =  sidereal.parseLat ( rawLat )
    except SyntaxError, detail:
        usage ( "Invalid latitude: %s" % detail )

    #-- 4 --
    # [ if rawLon is a valid longitude ->
    #     lon  :=  that longitude in radians
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        lon  =  sidereal.parseLon ( rawLon )
    except SyntaxError, detail:
        usage ( "Invalid longitude: %s" % detail )

    #-- 5 --
    # [ if rawDT is a valid date-time string ->
    #     dt  :=  that date-time as a datetime.datetime instance
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        dt  =  sidereal.parseDatetime ( rawDT )
    except SyntaxError, detail:
        usage ( "Invalid timestamp: %s" % detail )

    #-- 6 --
    latLon  =  sidereal.LatLon ( lat, lon )
    return  (altAz, latLon, dt)
</programlisting>
    </section> <!--aard-checkArgs-->
    <section id='aard-usage'>
      <title><code >usage()</code >: Print a message and stop</title>
      <programlisting role='outFile:aard'
># - - -   u s a g e

def usage ( *L ):
    """Print a usage message and stop.

      [ L is a list of strings ->
          sys.stderr  +:=  (usage message) + (elements of L,
                           concatenated)
          stop execution ]
    """
    print >>sys.stderr, "*** Usage:"
    print >>sys.stderr, "***   aard az+alt lat lon datetime"
    print >>sys.stderr, "*** Error: %s" % "".join(L)
    raise SystemExit
</programlisting>
    </section> <!--aard-usage-->
    <section id='aard-checkAltAz'>
      <title><code >checkAltAz()</code >: Validate horizon
      coordinates</title>
      <programlisting role='outFile:aard'
># - - -   c h e c k A l t A z

def checkAltAz ( rawAltAz ):
    """Check and convert a pair of horizon coordinates.

      [ rawAltAz is a string ->
          if rawAltAz is a valid set of horizon coordinates ->
            return those coordinates as a sidereal.AltAz instance
          else ->
            sys.stderr  +:=  error message
            stop execution ]
    """
</programlisting>
      <para>
        This function is quite similar to <xref
        linkend='rdaa-checkRADec' />: it looks for the plus or
        minus sign separating the azimuth from the altitude,
        using <code >SIGN_PAT</code >.  Then the two parts are
        processed by <xref linkend='parseAngle' />.
      </para>
      <programlisting role='outFile:aard'
>    #-- 1 --
    # [ if rawAltAz contains either a '+' or a '-' ->
    #     m  :=  a re.match instance describing the first matching
    #            character
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    m  =  SIGN_PAT.search ( rawAltAz )
    if  m is None:
        usage ( "Equatorial coordinates must be separated by "
                "'+' or '-'." )
    #-- 2 --
    # [ rawAz  :=  rawAltAz up to the match described by m
    #   sign  :=  characters matched by m
    #   rawAlt  :=  rawAltAz past the match described by m ]
    rawAz  =  rawAltAz[:m.start()]
    sign  =  m.group()
    rawAlt  =  rawAltAz[m.end():]

    #-- 3 --
    # [ if rawAz is a valid angle ->
    #     az  :=  that angle as radians
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        az  =  sidereal.parseAngle ( rawAz )
    except SyntaxError, detail:
        usage ( "Azimuth '%s' should have the form "
                "'NNNd[NNm[NN.NNNs]]'." % rawAz )

    #-- 4 --
    # [ if rawAlt is a valid angle ->
    #     alt  :=  that angle as radians
    #   else ->
    #     sys.stderr  +:=  error message
    #     stop execution ]
    try:
        absAlt  =  sidereal.parseAngle ( rawAlt )
    except SyntaxError, detail:
        usage ( "Altitude '%s' should have the form "
                "'NNd[NNm[NN.NNNs]]'." % rawAlt )

    #-- 5 --
    if  sign == '-':   alt  =  - absAlt
    else:              alt  =  absAlt

    #-- 6 --
    return  sidereal.AltAz ( alt, az )
</programlisting>
    </section> <!--aard-checkAltAz-->
    <section id='aard-epilogue'>
      <title>Epilogue</title>
      <programlisting role='outFile:aard'
>#================================================================
# Epilogue
#----------------------------------------------------------------

if  __name__ == "__main__":
    main()
</programlisting>
    </section> <!--aard-epilogue-->
  </section> <!--aard-->
  <section id='prologue'>
    <title>&py;: Prologue</title>
    <para>
      The &py; module begins with a documentation string that
      refers the reader back to this document.
    </para>
    <programlisting role='outFile:&spy;'
>"""&spy;: A Python module for astronomical calculations.

  For documentation, see:
    &selfURL;
"""
</programlisting>
  </section> <!--prologue-->
  <section id='imports'>
    <title>Imports</title>
    <para>
      This module relies on several Python library modules.
      First, we'll need all the standard trig functions and other
      math from the <code >math</code > module.
    </para>
    <programlisting role='outFile:&spy;'
>#================================================================
# Imports
#----------------------------------------------------------------

from math import *
</programlisting>
    <para>
      Next, we'll need the Python regular expression module.
    </para>
    <programlisting role='outFile:&spy;'
>import re
</programlisting>
    <para>
      Python's standard <code >datetime</code > module, available
      since Python version 2.3, contains a full set of function
      for manipulating dates and times.  There are references to
      this module's <code >datetime</code >, <code >date</code >,
      and <code >time</code > objects throughout this module.
      Refer to the <ulink
      url='http://docs.python.org/lib/module-datetime.html'
      >online documentation for <code >datetime</code ></ulink >.
    </para>
    <programlisting role='outFile:&spy;'
>import datetime
</programlisting>
  </section> <!--imports-->
  <section id='constants'>
    <title>Manifest constants</title>
    <para>
      Names of constants are in capital letters with
      &#x201c;<code >_</code >&#x201d; between words.
    </para>
    <section id='FIRST_GREGORIAN_YEAR'>
      <title><code >FIRST_GREGORIAN_YEAR</code ></title>
      <para>
        The year in which we changed from the flawed Julian
        calendar to the modern Gregorian calendar.
      </para>
      <programlisting role='outFile:&spy;'
>#================================================================
# Manifest constants
#----------------------------------------------------------------

FIRST_GREGORIAN_YEAR  =  1583
</programlisting>
    </section> <!--FIRST_GREGORIAN_YEAR-->
    <section id='TWO_PI'>
      <title><code >TWO_PI</code ></title>
      <para>
        Number of radians in a circle; used for normalizing
        angles.
      </para>
      <programlisting role='outFile:&spy;'
>TWO_PI  =  2.0 * pi
</programlisting>
    </section> <!--TWO_PI-->
    <section id='PI_OVER_12'>
      <title><code >PI_OVER_12</code ></title>
      <para>
        Multiply hours (one hour is 15&#x00b0;) by this value to
        convert to radians: there are 24 hours in 2&#x03c0; radians.
      </para>
      <programlisting role='outFile:&spy;'
>PI_OVER_12  =  pi / 12.0
</programlisting>
    </section> <!--PI_OVER_12-->
    <section id='JULIAN_BIAS'>
      <title><code >JULIAN_BIAS</code ></title>
      <para>
        This value is subtracted from Julian dates internally in
        the <code >JulianDate</code > class in order to get more
        precision.
      </para>
      <programlisting role='outFile:&spy;'
>JULIAN_BIAS  =  2200000    # 2,200,000
</programlisting>
    </section> <!--JULIAN_BIAS-->
    <section id='SIDEREAL_A'>
      <title><code >SIDEREAL_A</code >: Sidereal time conversion
      factor</title>
      <para>
        This constant is used in two places: <xref
        linkend='SiderealTime-utc' /> and <xref 
        linkend='SiderealTime-fromDatetime' />.  In <link
        linkend='references' >Duffett-Smith</link >, it is
        defined on page 23.
      </para>
      <programlisting role='outFile:&spy;'
>SIDEREAL_A  =  0.0657098
</programlisting>
    </section> <!--SIDEREAL_A-->
    <section id='FLOAT_PAT'>
      <title><code >FLOAT_PAT</code >: Regular expression for a
      floating-point number</title>
      <para>
        This compiled regular expression matches a floating-point
        number: one or more digits, followed optionally by a decimal
        point and zero or more digits.
      </para>
      <programlisting role='outFile:&spy;'
>FLOAT_PAT  =  re.compile (
    r'\d+'          # Matches one or more digits
    r'('            # Start optional fraction
      r'[.]'          # Matches the decimal point
      r'\d+'          # Matches one or more digits
    r')?' )          # End optional group
</programlisting>
    </section> <!--FLOAT_PAT-->
    <section id='D_PAT'>
      <title><code >D_PAT</code >: Degrees code</title>
      <para>
        This regular expression matches &#x201c;<code >d</code
        >&#x201d; or &#x201c;<code >D</code >&#x201d;.  It is used in
        parsing angles.
      </para>
      <programlisting role='outFile:&spy;'
>D_PAT  =  re.compile ( r'[dD]' )
</programlisting>
    </section> <!--D_PAT-->
    <section id='M_PAT'>
      <title><code >M_PAT</code >: Minutes code</title>
      <para>
        This regular expression matches &#x201c;<code >m</code
        >&#x201d; or &#x201c;<code >M</code >&#x201d;.  It is used in
        parsing angles.
      </para>
      <programlisting role='outFile:&spy;'
>M_PAT  =  re.compile ( r'[mM]' )
</programlisting>
    </section> <!--M_PAT-->
    <section id='S_PAT'>
      <title><code >S_PAT</code >: Seconds code</title>
      <para>
        This regular expression matches &#x201c;<code >s</code
        >&#x201d; or &#x201c;<code >S</code >&#x201d;.  It is used in
        parsing angles.
      </para>
      <programlisting role='outFile:&spy;'
>S_PAT  =  re.compile ( r'[sS]' )
</programlisting>
    </section> <!--S_PAT-->
    <section id='H_PAT'>
      <title><code >H_PAT</code >: Hours code</title>
      <para>
        This regular expression matches &#x201c;<code >h</code
        >&#x201d; or &#x201c;<code >H</code >&#x201d;.  It is used in
        parsing quantities in hours.
      </para>
      <programlisting role='outFile:&spy;'
>H_PAT  =  re.compile ( r'[hH]' )
</programlisting>
    </section> <!--H_PAT-->
    <section id='NS_PAT'>
      <title><code >NS_PAT</code >: Latitude suffix</title>
      <para>
        This regular expression is for the characters that
        describe north or south latitude.
      </para>
      <programlisting role='outFile:&spy;'
>NS_PAT  =  re.compile ( r'[nNsS]' )
</programlisting>
    </section> <!--NS_PAT-->
    <section id='EW_PAT'>
      <title><code >EW_PAT</code >: Longitude suffix</title>
      <para>
        A regular expression for the characters that describe
        east or west longitude.
      </para>
      <programlisting role='outFile:&spy;'
>EW_PAT  =  re.compile ( r'[eEwW]' )
</programlisting>
    </section> <!--EW_PAT-->
  </section> <!--constants-->
  <section id='functions'>
    <title>Exported functions</title>
    <para>
      These functions are available to users of the module.
    </para>
    <section id='hoursToRadians'>
      <title><code >hoursToRadians()</code >: Convert hours to
      radians</title>
      <programlisting role='outFile:&spy;'
># - - -   h o u r s T o R a d i a n s

def hoursToRadians ( hours ):
    """Convert hours (15 degrees) to radians.
    """
    return  hours * PI_OVER_12
</programlisting>
    </section> <!--hoursToRadians-->
    <section id='radiansToHours'>
      <title><code >radiansToHours()</code >: Convert hours to
      radians</title>
      <programlisting role='outFile:&spy;'
># - - -   r a d i a n s T o H o u r s

def radiansToHours ( radians ):
    """Convert radians to hours (15 degrees).
    """
    return  radians / PI_OVER_12
</programlisting>
    </section> <!--radiansToHours-->
    <section id='hourAngleToRA'>
      <title><code >hourAngleToRA()</code >: Convert an hour
      angle to right ascension</title>
      <para>
        For a given hour angle <code >h</code >, a given time
        <code >ut</code >, and an observer's longitude <code
        >eLong</code >, this function returns the right ascension
        in radians.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   h o u r A n g l e T o R A

def hourAngleToRA ( h, ut, eLong ):
    """Convert hour angle to right ascension.

      [ (h is an hour angle in radians as a float) and
        (ut is a timestamp as a datetime.datetime instance) and
        (eLong is an east longitude in radians) ->
          return the right ascension in radians corresponding
          to that hour angle at that time and location ]
    """
</programlisting>
      <para>
        This is <link linkend='references' >Duffett-Smith</link
        >'s algorithm 24, &#x201c;Converting between right
        ascension and hour-angle.&#x201d; The formula relating
        hour-angle <code ><replaceable >H</replaceable ></code >
        and right ascension &#x03b1; is:
      </para>
      <informalequation>
        <!--Source file t24-1.tex.
         !-->
        <mediaobject>
          <imageobject role="html">
            <imagedata fileref="t24-1.jpg"/>
          </imageobject>
          <imageobject role="fo">
            <imagedata fileref="t24-1.pdf"/>
          </imageobject>
        </mediaobject>
      </informalequation>
      <para>
        The first step is to convert <code >ut</code > into
        Greenwich Sidereal Time, GST.  See <xref
        linkend='SiderealTime-fromDatetime' />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ gst  :=  the Greenwich Sidereal Time equivalent to
    #            ut, as a SiderealTime instance ]
    gst  =  SiderealTime.fromDatetime ( ut )
</programlisting>
      <para>
        Next we convert GST to LST, local sidereal time.  See <xref
        linkend='SiderealTime-lst' />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 2 --
    # [ lst  :=  the local time corresponding to gst at
    #            longitude eLong ]
    lst  =  gst.lst ( eLong )
</programlisting>
      <para>
        Finally we subtract <code >H</code > from LST,
        normalizing the result to [0,2&#x03c0;).
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 3 --
    # [ alpha  :=  lst - h, normalized to [0,2*pi) ]
    alpha  =  (lst.radians - h) % TWO_PI

    #-- 4 --
    return alpha
</programlisting>
    </section> <!--hourAngleToRA-->
    <section id='raToHourAngle'>
      <title><code >raToHourAngle()</code >: Convert a right
      ascension to an hour angle</title>
      <para>
        This is the inverse of <xref linkend='raToHourAngle' />.
        The first two steps are the same: derive the GST and
        convert it to LST.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   r a T o H o u r A n g l e

def raToHourAngle ( ra, ut, eLong ):
    """Convert right ascension to hour angle.

      [ (ra is a right ascension in radians as a float) and
        (ut is a timestamp as a datetime.datetime instance) and
        (eLong is an east longitude in radians) ->
          return the hour angle in radians at that time and
          location corresponding to that right ascension ]
    """
    #-- 1 --
    # [ gst  :=  the Greenwich Sidereal Time equivalent to
    #            ut, as a SiderealTime instance ]
    gst  =  SiderealTime.fromDatetime ( ut )

    #-- 2 --
    # [ lst  :=  the local time corresponding to gst at
    #            longitude eLong ]
    lst  =  gst.lst ( eLong )
</programlisting>
      <para>
        All that remains is to subtract <code >h</code > from
        <code >lst</code > and normalize the result to
        [0,2&#x03c0;).
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 3 --
    # [ h  :=  lst - ra, normalized to [0,2*pi) ]
    h  =  (lst.radians - ra) % TWO_PI

    #-- 4 --
    return h
</programlisting>
    </section> <!--raToHourAngle-->
    <section id='dayNo'>
      <title><code >dayNo()</code >: Date to day number</title>
      <para>
        This method returns the number of days between the given
        date <code >dt</code >, as either a <code
        >datetime.date</code > or <code >datetime.datetime </code
        > instance, and January 0 of that year.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   d a y N o

def dayNo ( dt ):
    """Compute the day number within the year.

      [ dt is a date as a datetime.datetime or datetime.date ->
          return the number of days between dt and Dec. 31 of
          the preceding year ]
    """
</programlisting>
      <para>
        This computation will use the Python <code
        >datetime</code > package's concept of the
        &#x201c;proleptic Gregorian ordinal,&#x201d; which is a
        day number based on January 1, 1AD.  Both <code
        >datetime.date</code > and <code >datetime.datetime</code
        > instances have a <code >.toordinal()</code > method
        that returns this number.
      </para>
      <para>
        The <code >datetime.date()</code > constructor will not
        accept a date of January 0 (&#x201c;ValueError: day is
        out of range for month&#x201d;), so we'll subtract the
        ordinal of January 1, and then add 1.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ dateOrd  :=  proleptic Gregorian ordinal of dt
    #   jan1Ord  :=  proleptic Gregorian ordinal of January 1
    #                of year (dt.year) ]
    dateOrd  =  dt.toordinal()
    jan1Ord  =  datetime.date ( dt.year, 1, 1 ).toordinal()

    #-- 2 --
    return  dateOrd - jan1Ord + 1
</programlisting>
    </section> <!--dayNo-->
    <section id='parseDatetime'>
      <title><code >parseDatetime()</code >: Convert an external
      date-time string</title>
      <para>
        A date-time string consists of a date string, optionally
        followed by letter &#x201c;<code >T</code >&#x201d;
        (case-insensitive) and a time string.  <code
        >T_PATTERN</code > is a regular expression that matches
        either case of &#x201c;<code >T</code >&#x201d;.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e D a t e t i m e

T_PATTERN = re.compile ( '[tT]' )

def parseDatetime ( s ):
    """Parse a date with optional time.

      [ s is a string ->
          if s is a valid date with optional time ->
            return that timestamp as a datetime.datetime instance
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        In general, date-time expressions can be quite complex.
        Using Python's <code >re</code > package, we might write
        a regular expression to match all the cases, but it would
        be huge.  Just for conceptual clarity, we'll break into
        two sub-problems: we'll use <xref linkend='parseDate' />
        to process the date, and we'll use <xref
        linkend='parseTime' /> to process the time if there is one.
      </para>
      <para>
        If <code >s</code > does not contain letter &#x201c;<code
        >T</code >&#x201d; or &#x201c;<code >t</code >&#x201d;,
        then it must represent just a date; use <code
        >parseDate()</code > to convert it.  If there is a
        &#x201c;<code >T</code >&#x201d;, break the string in two
        at that point, send the pieces to <code
        >parseDate()</code > and <code >parseTime()</code >, and
        reassemble them.
      </para>
      <para>
        The <code >.search()</code > method on a compiled regular
        expression finds the first occurrence of a pattern
        anywhere in its argument; the returned <code >match</code
        > object has methods <code >.start()</code > and <code
        >.end()</code > that delineate where the match occurred.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ if s contains "T" or "t" ->
    #     rawDate  :=  s up to the first such character
    #     rawTime  :=  s from just after the first such
    #                  character to the end
    #   else ->
    #     rawDate  :=  s
    #     rawTime  :=  None ]
    m = T_PATTERN.search ( s )
    if  m is None:
        rawDate, rawTime  =  s, None
    else:
        rawDate  =  s[:m.start()]
        rawTime  =  s[m.end():]
</programlisting>
      <para>
        The <code >parseDate()</code > function will handle
        checking and conversion of the date part.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 2 --
    # [ if rawDate is a valid date ->
    #     datePart  :=  rawDate as a datetime.datetime instance
    #   else -> raise SyntaxError ]
    datePart  =  parseDate ( rawDate )
</programlisting>
      <para>
        If <code >rawTime</code > is not <code >None</code >, we
        call <xref linkend='parseTime' /> to validate and convert
        it, and store the result in <code >timePart</code >.  If
        <code >rawTime</code > is <code >None</code >, there is
        no time, so we'll set <code >timePart</code > to a <code
        >datetime.time</code > instance representing 00:00.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 3 --
    # [ if rawTime is None ->
    #     timePart  :=  00:00 as a datetime.time
    #   else if rawTime is valid ->
    #     timePart  :=  rawTime as a datetime.time
    #   else -> raise SyntaxError ]
    if  rawTime is None:
        timePart  =  datetime.time ( 0, 0 )
    else:
        timePart  =  parseTime ( rawTime )
</programlisting>
      <para>
        The static <code >.combine()</code > method of class
        <code >datetime.datetime</code > takes a <code
        >datetime.date</code > and a <code >datetime.time</code >
        and combines them to produce a <code
        >datetime.datetime</code > instance.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 4 --
    return  datetime.datetime.combine ( datePart, timePart )
</programlisting>
    </section> <!--parseDatetime-->
    <section id='parseDate'>
      <title><code >parseDate()</code >: Convert an external date
      string</title>
      <para>
        This function checks a string <code >s</code > to see if
        it is a valid date string as described in <ulink
        url='&specURL;parseDate.html' >the specification</ulink
        >.
      </para>
      <para>
        Although the ISO standard requires left zeroes on all
        three units, we'll be a little lax about the month and
        year.  However, to prevent a repetition of that ugly Y2K
        business, we'll absolutely require four digits for the
        year.  The compiled regular expression <code
        >DATE_PAT</code > implements these rules. <code
        >YEAR_FIELD</code >, <code >MONTH_FIELD</code >, and
        <code >DAY_FIELD</code > are the group names used to
        retrieve the values of the year, month, and day once the
        pattern has matched.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e D a t e

YEAR_FIELD  =  "Y"
MONTH_FIELD  =  "M"
DAY_FIELD  =  "D"

dateRe  =  (
    r'('            # Begin YEAR_FIELD
      r'?P&lt;%s>'       # Name this group YEAR_FIELD
      r'\d{4}'        # Match exactly four digits
    r')'            # End YEAR_FIELD
    r'\-'           # Matches one hyphen
    r'('            # Begin MONTH_FIELD
      r'?P&lt;%s>'       # Name this group MONTH_FIELD
      r'\d{1,2}'      # Matches one or two digits
    r')'            # End MONTH_FIELD
    r'\-'           # Matches "-"
    r'('            # Begin DAY_FIELD
      r'?P&lt;%s>'       # Name this group DAY_FIELD
      r'\d{1,2}'      # Matches one or two digits
    r')'            # End DAY_FIELD
    r'$'            # Make sure all characters match
    ) % (YEAR_FIELD, MONTH_FIELD, DAY_FIELD)
DATE_PAT  =  re.compile ( dateRe )

def parseDate ( s ):
    """Validate and convert a date in external form.

      [ s is a string ->
          if s is a valid external date string ->
            return that date as a datetime.date instance
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        If <code >DATE_PAT</code > matches <code >s</code >,
        we'll get back a <code >match</code > instance; otherwise
        we get back <code >None</code >.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ if DATE_PAT matches s ->
    #     m  :=  a match instance describing the match
    #   else -> raise SyntaxError ]
    m  =  DATE_PAT.match ( s )
    if  m is None:
        raise SyntaxError, ( "Date does not have pattern YYYY-DD-MM: "
                             "'%s'" % s )
</programlisting>
      <para>
        We retrieve the contents of the three fields using the
        <code >match</code > instance's <code >.group()</code >
        method, then construct and return a <code
        >datetime.date</code > instance.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 2 --
    year   =  int ( m.group ( YEAR_FIELD ) )
    month  =  int ( m.group ( MONTH_FIELD ) )
    day    =  int ( m.group ( DAY_FIELD ) )

    #-- 3 --
    return  datetime.date ( year, month, day )
</programlisting>
    </section> <!--parseDate-->
    <section id='parseTime'>
      <title><code >parseTime()</code >: Convert an external time
      string</title>
      <para>
        This function validates and converts an external time of
        day <code >s</code >, with optional time zone modifier,
        as described in <ulink url='&specURL;parseTime.html'
        ></ulink >.
      </para>
      <para>
        Rather than try to do all the parsing and breaking out of
        pieces with one big regular expression, we'll use a mixed
        strategy that goes roughly like this:
      </para>
      <orderedlist spacing="compact">
        <listitem>
          <para>
            The string <code >s</code > must start with one or
            more digits, optionally followed by a decimal point
            and more digits.  Remove this and save it as a <code
            >float</code > in <code >decHour</code >.
          </para>
        </listitem>
        <listitem>
          <para>
            If what is left starts with &#x201c;<code >:</code
            >&#x201d;, remove that, followed by another float
            number, and save as as <code >decMinute</code >.
          </para>
          <para>
            Similarly, if what follows <emphasis >that</emphasis
            > is another &#x201c;<code >:</code >&#x201d;, remove
            the second colon and save the number that must follow
            in <code >decSecond</code >.
          </para>
        </listitem>
        <listitem>
          <para>
            If there is still something left, it must be a time
            zone modifier.  Call <xref linkend='parseZone' /> to
            validate and convert that to an instance of a class
            that inherits from <code >datetime.tzinfo</code >.
          </para>
        </listitem>
        <listitem>
          <para>
            If there is anything left at this point, it is an
            error.
          </para>
        </listitem>
      </orderedlist>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e T i m e

def parseTime ( s ):
    """Validate and convert a time and optional zone.

      [ s is a string ->
          if s is a valid time with optional zone suffix ->
            return that time as a datetime.time
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        The first order of business is to remove the time from the
        front of the string.  We use <xref linkend='parseFloat' /> to
        match each floating-point number.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 -
    # [ if s starts with FLOAT_PAT ->
    #     decHour  :=  matching part of s as a float
    #     minuteTail  :=  part s past the match
    #   else -> raise SyntaxError ]
    decHour, minuteTail  =  parseFloat ( s, "Hour number"  )
</programlisting>
      <para>
        If <code >minuteTail</code > starts with a colon, remove
        the minutes.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 2 --
    # [ if minuteTail starts with ":" followed by FLOAT_PAT ->
    #     decMinute  :=  part matching FLOAT_PAT as a float
    #     secondTail  :=  part of minuteTail after the match
    #   else if minuteTail starts with ":" not followed by
    #   FLOAT_PAT ->
    #     raise SyntaxError
    #   else ->
    #     decMinute  :=  0.0
    #     secondTail  :=  minuteTail ]
    if  minuteTail.startswith(':'):
        m  =  FLOAT_PAT.match ( minuteTail[1:] )
        if  m is None:
            raise SyntaxError, ( "Expecting minutes: '%s'" %
                                 minuteTail )
        else:
            decMinute  =  float(m.group())
            secondTail  =  minuteTail[m.end()+1:]
    else:
        decMinute  =  0.0
        secondTail  =  minuteTail
</programlisting>
      <para>
        If <code >secondTail</code > starts with a colon, remove
        the seconds.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 3 --
    # [ if secondTail starts with ":" followed by FLOAT_PAT ->
    #     decSecond  :=  part matching FLOAT_PAT as a float
    #     zoneTail  :=  part of secondTail after the match
    #   else if secondTail starts with ":" not followed by
    #   FLOAT_PAT ->
    #     raise SyntaxError
    #   else ->
    #     decSecond  :=  0.0
    #     zoneTail  :=  secondTail ]
    if  secondTail.startswith(':'):
        m  =  FLOAT_PAT.match ( secondTail[1:] )
        if  m is None:
            raise SyntaxError, ( "Expecting seconds: '%s'" %
                                 secondTail )
        else:
            decSecond  =  float(m.group())
            zoneTail  =  secondTail[m.end()+1:]
    else:
        decSecond  =  0.0
        zoneTail  =  secondTail
</programlisting>
      <para>
        If anything is left, it had better be a zone.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 4 --
    # [ if zoneTail is empty ->
    #     tz  :=  None
    #   else if zoneTail is a valid zone suffix ->
    #     tz  :=  that zone information as an instance of a class
    #             that inherits from datetime.tzinfo
    #   else -> raise SyntaxError ]
    if  len(zoneTail) == 0:
        tz  =  None
    else:
        tz  =  parseZone ( zoneTail )
</programlisting>
      <para>
        All that remains is to assemble the parts.  At this point
        we have three numbers for hours, minutes, and seconds,
        any of which may not be integral.  In order to conform
        to the way the <code >datetime</code > module likes to
        see times, we'll convert that to decimal hours, then back
        to mixed units, using <xref linkend='dmsUnits' />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 5 --
    # [ hours  :=  decHour + decMinute/60.0 + decSecond/3600.0 ]
    hours  =  dmsUnits.mixToSingle ( (decHour, decMinute, decSecond) )
</programlisting>
      <para>
        The <code >datetime.time()</code > constructor wants integer
        hours, minutes, seconds, and microseconds.  The <code
        >dmsUnits.singleToMix()</code > method returns integer hours,
        integer minutes, and floating seconds; we'll do the remaining
        two conversions explicitly.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 6 --
    # [ return a datetime.time representing hours ]
    hh, mm, seconds = dmsUnits.singleToMix ( hours )
    wholeSeconds, fracSeconds = divmod ( seconds, 1.0 )
    ss = int(wholeSeconds)
    usec = int ( fracSeconds * 1e6 )
    return  datetime.time ( hh, mm, ss, usec, tz )
</programlisting>
    </section> <!--parseTime-->
    <section id='parseZone'>
      <title><code >parseZone()</code >: Process a time zone
      suffix</title>
      <para>
        For the various forms allowed as time zone suffixes, refer to
        <ulink url='&specURL;parseTime' >the specification</ulink >.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e Z o n e

def parseZone ( s ):
    """Validate and convert a time zone suffix.

      [ s is a string ->
          if s is a valid time zone suffix ->
            return that zone's information as an instance of
            a class that inherits from datetime.tzinfo
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        The only supported zone suffixes with variable content are the
        &#x201c;<code >+hhmm</code >&#x201d; and &#x201c;<code
        >-hhmm</code >&#x201d; forms.  The rest can be handled with a
        dictionary whose keys are the various time zone codes
        (uppercased, so we can be case-insensitive), and each
        corresponding value is the appropriate <code >tzinfo</code >
        instance.  For this dictionary, see <xref linkend='zoneCodeMap' />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ if s starts with "+" or "-" and is a valid fixed-offset
    #   time zone suffix ->
    #     return that zone's information as a datetime.tzinfo instance
    #   else if is starts with "+" or "-" but is not a valid
    #   fixed-offset time zone suffix ->
    #     raise SyntaxError
    #   else -> I ]
    if  s.startswith("+") or s.startswith("-"):
        return  parseFixedZone ( s )

    #-- 2 --
    # [ if s.upper() is a key in zoneCodeMap ->
    #     return the corresponding value
    #   else -> raise SyntaxError ]
    try:
        tz  =  zoneCodeMap[s.upper()]
        return tz
    except KeyError:
        raise SyntaxError, ( "Unknown time zone code: '%s'" % s )
</programlisting>
    </section> <!--parseZone-->
    <section id='parseFixedZone'>
      <title><code >parseFixedZone()</code >: Parse fixed zone
      suffix</title>
      <para>
        If the argument <code >s</code > is a fixed time zone offset
        of the form &#x201c;<code >&#x00b1;hhmm</code >&#x201d;, this
        function returns an instance of class <code >FixedZone</code >
        representing that offset.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e F i x e d Z o n e

HHMM_PAT  =  re.compile (
    r'\d{4}'    # Matches exactly four digits
    r'$' )        # Be sure everything is matched

def parseFixedZone ( s ):
    """Convert a +hhmm or -hhmm zone suffix.

      [ s is a string ->
          if s is a time zone suffix of the form "+hhmm" or "-hhmm" ->
            return that zone information as an instance of a class
            that inherits from datetime.tzinfo
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        The first character must be &#x201c;<code >+</code >&#x201d;
        or &#x201c;<code >-</code >&#x201d;; we set <code >sign</code
        > to +1 or -1, respectively.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    if  s.startswith('+'):    sign  =  1
    elif  s.startswith('-'):  sign  =  -1
    else:
        raise SyntaxError, ( "Expecting zone modifier as %shhmm: "
                             "'%s'" % (s[0], s) )
</programlisting>
      <para>
        Use a regular expression to check the length and
        content of the hours and minutes.  Then return an
        instance of the <code >FixedZone</code > class representing
        that number of hours and minutes.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 2 --
    # [ if s[1:] matches HHMM_PAT ->
    #     hours  :=  the HH part as an int
    #     minutes  :=  the MM part as an int
    #   else -> raise SyntaxError ]
    rawHHMM  =  s[1:]
    m  =  HHMM_PAT.match ( rawHHMM )
    if  m is None:
        raise SyntaxError, ( "Expecting zone modifier as %sHHMM: "
                             "'%s'" % (s[0], s) )
    else:
        hours  =  int ( rawHHMM[:2] )
        minutes  =  int ( rawHHMM[2:] )

    #-- 3 --
    return  FixedZone ( sign*hours, sign*minutes, s )
</programlisting>
    </section> <!--parseFixedZone-->
    <section id='class-FixedZone'>
      <title><code >class FixedZone</code >: Fixed-offset time zone</title>
      <para>
        This class represents a time zone with a fixed offset in hours
        and minutes east of UTC.  It is a concrete class implementing
        the abstract class <code >datetime.tzinfo</code >, so it
        must implement concrete methods <code >utcoffset()</code >,
        <code >tzname()</code >, and <code >dst()</code >.
      </para>
      <programlisting role='outFile:&spy;'>
# - - - - -   c l a s s   F i x e d Z o n e

DELTA_ZERO  =  datetime.timedelta(0)
DELTA_HOUR  =  datetime.timedelta(hours=1)

class FixedZone(datetime.tzinfo):
    """Represents a time zone with a fixed offset east of UTC.

      Exports:
        FixedZone ( hours, minutes, name ):
          [ (hours is a signed offset in hours as an int) and
            (minutes is a signed offset in minutes as an int) ->
              return a new FixedZone instance representing
              those offsets east of UTC ]
      State/Invariants:
        .__offset:
          [ a datetime.timedelta representing self's offset
            east of UTC ]
        .__name:
          [ as passed to the constructor's name argument ]
    """
</programlisting>
      <para>
        This class is an adaptation of the <code >FixedOffset</code >
        class shown in the examples section of <ulink
        url='http://docs.python.org/lib/datetime-tzinfo.html' >the
        reference material for <code >datetime.tzinfo</code ></ulink
        >.
      </para>
      <para>
        The constructor combines the <code >hours</code > and <code
        >minutes</code > arguments into a <code
        >datetime.timedelta</code > instance and stores them in the
        instance.
      </para>
      <programlisting role='outFile:&spy;'
>    def __init__ ( self, hh, mm, name ):
        """Constructor for FixedZone.
        """
        self.__offset  =  datetime.timedelta ( hours=hh, minutes=mm )
        self.__name  =  name
</programlisting>
      <para>
        The <code >.utcoffset()</code > method returns the offset east
        of UTC.
      </para>
      <programlisting role='outFile:&spy;'
>    def utcoffset(self, dt):
        """Return self's offset east of UTC.
        """
        return  self.__offset
</programlisting>
      <para>
        The <code >.tzname()</code > method returns the zone's name.
      </para>
      <programlisting role='outFile:&spy;'
>    def  tzname(self, dt):
        """Return self's name.
        """
        return  self.__name
</programlisting>
      <para>
        The <code >.dst()</code > method always returns <code
        >DELTA_ZERO</code >, since a fixed time zone by definition
        never has daylight time.
      </para>
      <programlisting role='outFile:&spy;'
>    def  dst(self, dt):
        """Return self's daylight time offset.
        """
        return  DELTA_ZERO
</programlisting>
    </section> <!--class-FixedZone-->
    <section id='class-USTimeZone'>
      <title><code >class USTimeZone</code >: Time zone with daylight
      time</title>
      <para>
        This class implements the daylight saving time rules for the
        USA, honoring the change that went into effect in 2007.
        Dates before 2007 are assumed to use the old rules.
      </para>
      <para>
        Daylight time always changes on a Sunday.  To compute the
        dates of those Sundays in a particular year, we will need the
        following function.  Given a <code >datetime.date</code >
        instance <code >dt</code >, it computes the date of
        the first Sunday on or after that date, and returns
        the result as a <code >datetime.date</code >.
      </para>
      <programlisting role='outFile:&spy;'
>def firstSundayOnOrAfter ( dt ):
    """Find the first Sunday on or after a given date.

      [ dt is a datetime.date ->
          return a datetime.date representing the first Sunday
          on or after dt ]
    """
</programlisting>
      <para>
        Because the <code >.weekday()</code > method returns a number
        that represents Sunday as 6, we subtract that number from 6 to
        get the number of days between dt and the next Sunday.
        Then we use a <code >datetime.timedelta</code > instance
        to add to <code >dt</code > to yield the Sunday date.
      </para>
      <programlisting role='outFile:&spy;'
>    daysToGo  =  dt.weekday()
    if  daysToGo:
        dt  +=  datetime.timedelta ( daysToGo )
    return dt
</programlisting>
      <para>
        Here's the class itself.
      </para>
      <programlisting role='outFile:&spy;'>
# - - - - -   c l a s s   U S T i m e Z o n e

class USTimeZone(datetime.tzinfo):
    """Represents a U.S. time zone, with automatic daylight time.

      Exports:
        USTimeZone ( hh, mm, name, stdName, dstName ):
          [ (hh is an offset east of UTC in hours) and
            (mm is an offset east of UTC in minutes) and
            (name is the composite zone name) and
            (stdName is the non-DST name) and
            (dstName is the DST name) ->
              return a new USTimeZone instance with those values ]

      State/Invariants:
        .__offset:
          [ self's offset east of UTC as a datetime.timedelta ]
        .__name:      [ as passed to constructor's name ]
        .__stdName:   [ as passed to constructor's stdName ]
        .__dstName:   [ as passed to constructor's dstName ]
    """
</programlisting>
      <para>
        This class is an adaptation of the <code >USTimeZone</code >
        class shown in the examples section of <ulink
        url='http://docs.python.org/lib/datetime-tzinfo.html' >the
        reference material for <code >datetime.tzinfo</code ></ulink
        >, with modifications for the new daylight time rules.
      </para>
      <para>
        We'll need class variables for the calculation of the DST
        changeover dates.  In each case, we want a date such that we
        can plug in the given year and send it to the <code
        >firstSundayOnOrAfter()</code > function above to yield a DST
        changeover date.
      </para>
      <itemizedlist>
        <listitem>
          <para>
            <code >DST_START_OLD</code >: The pre-2007 rule for DST is
            that it starts on the first Sunday in April, at 2am.
            Hence, this date is April 1.  Applying <code
            >firstSundayOnOrAfter()</code > to this date gives us the
            first Sunday in April at 2am.
          </para>
          <programlisting role='outFile:&spy;'
>    DST_START_OLD  =  datetime.datetime ( 1, 4, 1, 2 )
</programlisting>
        </listitem>
        <listitem>
          <para>
            <code >DST_END_OLD</code >: The pre-2007 rule is that DST
            ends on the last Sunday of October, that is, the first
            Sunday on or after October 25.
          </para>
          <programlisting role='outFile:&spy;'
>    DST_END_OLD  =  datetime.datetime ( 1, 10, 25, 2 )
</programlisting>
        </listitem>
        <listitem>
          <para>
            <code >DST_START_2007</code >:  The new rule is that DST
            starts on the second Sunday in March, that is, the first
            Sunday on or after March 8.
          </para>
          <programlisting role='outFile:&spy;'
>    DST_START_2007  =  datetime.datetime ( 1, 3, 8, 2 )
</programlisting>
        </listitem>
        <listitem>
          <para>
            <code >DST_END_2007</code >:  Since 2007, DST ends on the
            first Sunday in November.
          </para>
          <programlisting role='outFile:&spy;'
>    DST_END_2007  =  datetime.datetime ( 1, 11, 1, 2 )
</programlisting>
        </listitem>
      </itemizedlist>
      <para>
        Here's the class constructor.
      </para>
      <programlisting role='outFile:&spy;'
>    def __init__ ( self, hh, mm, name, stdName, dstName ):
        self.__offset   =  datetime.timedelta ( hours=hh, minutes=mm )
        self.__name     =  name
        self.__stdName  =  stdName
        self.__dstname  =  dstName
</programlisting>
      <para>
        The <code >.tzname()</code > method returns the current name,
        which depends on whether the given time is DST or not.
      </para>
      <programlisting role='outFile:&spy;'
>    def tzname(self, dt):
        if  self.dst(dt):   return self.__dstName
        else:               return self.__stdName
</programlisting>
      <para>
        The <code >.utcoffset()</code > method returns the offset east
        of UTC, taking into account whether DST is in effect.  We
        use the fact that a <code >datetime.timedelta</code > with a
        zero offset is treated as false in Boolean contexts.
      </para>
      <programlisting role='outFile:&spy;'
>    def utcoffset(self, dt):
        return self.__offset + self.dst(dt)
</programlisting>
      <para>
        The <code >.dst()</code > method returns an offset as a <code
        >datetime.timedelta</code >, and determines whether DST is in effect.
      </para>
      <programlisting role='outFile:&spy;'
>    def dst(self, dt):
        """Return the current DST offset.

          [ dt is a datetime.date ->
              if  daylight time is in effect in self's zone on
              date dt ->
                return +1 hour as a datetime.timedelta
              else ->
                return 0 as a datetime.delta ]
        """
</programlisting>
      <para>
        This implementation will ignore the <code >.tzinfo</code >
        attribute of the <code >dt</code > argument, and just use
        <code >self</code >'s zone information.
      </para>
      <para>
        First we must find the starting and ending Sundays for DST for
        the year given by <code >dt</code >.  The <code
        >.replace()</code > method gives us a new <code
        >datetime</code > instance with the year taken from <code
        >dt</code >.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ dtStart  :=  Sunday when DST starts in year dt.year
        #   dtEnd    :=  Sunday when DST ends in year dt.year ]
        if  dt.year >= 2007:
            startDate  =  self.DST_START_2007.replace ( year=dt.year )
            endDate  =  self.DST_END_2007.replace ( year=dt.year )
        else:
            startDate  =  self.DST_START_OLD.replace ( year=dt.year )
            endDate  =  self.DST_END_OLD.replace ( year=dt.year )
        dtStart  =  firstSundayOnOrAfter ( startDate )
        dtEnd    =  firstSundayOnOrAfter ( endDate )
</programlisting>
      <para>
        The <code >datetime</code > module does not allow naive times
        to be compared to aware times, so we'll make a copy of <code
        >dt</code > with its time zone information removed.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 2 --
        # [ naiveDate  :=  dt with its tzinfo member set to None ]
        naiveDate  =  dt.replace ( tzinfo=None )
</programlisting>
      <para>
        Now we can find out if <code >naiveDate</code > is within the
        DST range, and return the appropriate offset.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        # [ if naiveDate is in the interval (dtStart, dtEnd) ->
        #     return DELTA_HOUR
        #   else ->
        #     return DELTA_ZERO ]
        if  dtStart &lt;= naiveDate &lt; dtEnd:
            return  DELTA_HOUR
        else:
            return  DELTA_ZERO
</programlisting>
    </section> <!--class-USTimeZone-->
    <section id='zoneCodeMap'>
      <title><code >zoneCodeMap</code >: Dictionary of time zones</title>
      <para>
        This dictionary is used in <xref linkend='parseZone' /> to
        convert time zone codes to <code >tzinfo</code > instances.
      </para>
      <programlisting role='outFile:&spy;'
>utcZone  =  FixedZone(0, 0, "UTC")

estZone  =  FixedZone(-5, 0, "EST")
edtZone  =  FixedZone(-4, 0, "EDT")
etZone   =  USTimeZone(-5, 0, "ET", "EST", "EDT")

cstZone  =  FixedZone(-6, 0, "CST")
cdtZone  =  FixedZone(-5, 0, "CDT")
ctZone   =  USTimeZone(-6, 0, "CT", "CST", "CDT")

mstZone  =  FixedZone(-7, 0, "MST")
mdtZone  =  FixedZone(-6, 0, "MDT")
mtZone   =  USTimeZone(-7, 0, "MT", "MST", "MDT")

pstZone  =  FixedZone(-8, 0, "PST")
pdtZone  =  FixedZone(-7, 0, "PDT")
ptZone   =  USTimeZone(-8, 0, "PT", "PST", "PDT")

zoneCodeMap  =  {
    "UTC": utcZone,
    "EST": estZone,    "EDT": edtZone,    "ET":  etZone,
    "CST": cstZone,    "CDT": cdtZone,    "CT":  ctZone,
    "MST": mstZone,    "MDT": mdtZone,    "MT":  mtZone,
    "PST": pstZone,    "PDT": pdtZone,    "PT":  ptZone }
</programlisting>
    </section> <!--zoneCodeMap-->
    <section id='parseAngle'>
      <title><code >parseAngle()</code >: Convert an external
      angle string</title>
      <para>
        This function validates and converts a string containing an
        angle in mixed units, such as &#x201c;<code >34d18.37m</code
        >&#x201d; or &#x201c;<code >34d18m4.006s</code >&#x201d;.  The
        degrees part is required; the minutes and seconds are
        optional.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e A n g l e

def parseAngle ( s ):
    """Validate and convert an external angle.

      [ s is a string ->
          if s is a valid external angle ->
            return s in radians
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        The general approach is first to require the degrees
        part&#x2014;a floating-point number&#x2014;followed by
        &#x201c;<code >d</code >&#x201d; (either case).  If there is
        anything after that, it must start with another float and
        letter &#x201c;<code >m</code >&#x201d;.  If there is still
        more, it must be another float and &#x201c;<code >s</code
        >&#x201d;, and nothing more.
      </para>
      <para>
        First we set up default values for the optional minutes and
        seconds.  Then we process the degrees and its suffix.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    minute  =  second  =  0.0
    #-- 2 --
    # [ if s starts with a float followed by 'd' or 'D' ->
    #     degree    :=  that float as type float
    #     minTail  :=  s after that float and suffix
    #   else -> raise SyntaxError ]
    degree, minTail  =  parseFloatSuffix ( s, D_PAT,
                          "Degrees followed by 'd'" )

    #-- 3 --
    # [ if minTail is empty -> I
    #   else if minTail has the form "(float)m" ->
    #     minute  :=  that (float)
    #   else if minTail has the form "(float)m(float)s" ->
    #     minute  :=  the first (float)
    #     second  :=  the second (float)
    #   else -> raise SyntaxError ]
    if  len(minTail) != 0:
        #-- 3.1 --
        # [ if minTail starts with a float followed by 'm' or 'M' ->
        #     minute  :=  that float as type float
        #     secTail  :=  minTail after all that
        #   else -> raise SyntaxError ]
        minute, secTail  =  parseFloatSuffix ( minTail, M_PAT,
                                "Minutes followed by 'm'" )

        #-- 3.2 --
        # [ if secTail is empty -> I
        #   else if secTail starts with a float followed by
        #   's' or 'S' ->
        #     second  :=  that float as type float
        #     checkTail  :=  secTail after all that
        #   else -> raise SyntaxError ]
        if  len(secTail) != 0:
            second, checkTail  =  parseFloatSuffix ( secTail,
                S_PAT, "Seconds followed by 's'" )
            if  len(checkTail) != 0:
                raise SyntaxError, ( "Unidentifiable angle parts: "
                                     "'%s'" % checkTail )
</programlisting>
      <para>
        To convert from mixed units, we use <xref linkend='dmsUnits'
        />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 4 --
    # [ return the angle (degree, minute, second) in radians ]
    angleDegrees  =  dmsUnits.mixToSingle ( (degree, minute, second) )
    return  radians ( angleDegrees )
</programlisting>
    </section> <!--parseAngle-->
    <section id='parseFloatSuffix'>
      <title><code >parseFloatSuffix</code >: Parse a number followed
      by a code</title>
      <para>
        This utility routine is used to handle the common case where a
        floating-point number must be followed by a specific letter,
        such as in the angle expression &#x201c;<code >107m</code
        >&#x201d;.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e F l o a t S u f f i x

def parseFloatSuffix ( s, codeRe, message ):
    """Parse a float followed by a letter code.

      [ (s is a string) and
        (codeRe is a compiled regular expression) and
        (message is a string describing what is expected) ->
          if  s starts with a float, followed by code (using 
          case-insensitive comparison) ->
            return (x, tail) where x is that float as type float
            and tail is the part of s after the float and code
          else -> raise SyntaxError, "Expecting (message)" ]
    """
</programlisting>
      <para>
        We use <xref linkend='parseFloat' /> to remove the required
        floating number from the front of <code >s</code >.  We could
        just pass through the <code >SyntaxError</code > exception
        that that routine will raise, but we catch and re-raise it
        here so as to provide more information about what is expected.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ if  s starts with a float ->
    #     x  :=  that float as type float
    #     codeTail  :=  the part of s after that float
    #   else -> raise SyntaxError, "Expecting (message)" ]
    x, codeTail  =  parseFloat ( s, message )

    #-- 2 --
    # [ if codeTail starts with code (case-insensitive) ->
    #     return (x, the part of codeTail after the match)
    #   else -> raise SyntaxError ]
    discard, tail  =  parseRe ( codeTail, codeRe, message )

    #-- 3 --
    return (x, tail)
</programlisting>
    </section> <!--parseFloatSuffix-->
    <section id='parseFloat'>
      <title><code >parseFloat()</code >: Parse a floating-point
      number</title>
      <para>
        A service routine, this function removes a floating-point
        number from the head of <code >s</code >.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e F l o a t

def parseFloat ( s, message ):
    """Parse a floating-point number at the front of s.

      [ (s is a string) and
        (message is a string describing what is expected) ->
          if s begins with a floating-point number ->
            return (x, tail) where x is the number as type float
            and tail is the part of s after the match
          else -> raise SyntaxError, "Expecting (message)" ]
    """
</programlisting>
      <para>
        We use the precompiled regular expression from <xref
        linkend='FLOAT_PAT' /> to match the number.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ if the front of s matches FLOAT_PAT ->
    #     m  :=  a Match object describing the match
    #   else -> raise SyntaxError ]
    rawFloat, tail  =  parseRe ( s, FLOAT_PAT, message )
</programlisting>
      <para>
        The matching string is available as <code >m.group()</code >.
        The position just after the match is available as <code
        >m.end()</code >.
      </para>
      <programlisting role='outFile:&spy;'
>
    #-- 2 --
    return  (float(rawFloat), tail)
</programlisting>
    </section> <!--parseFloat-->
    <section id='parseRe'>
      <title><code >parseRe()</code >: Parse a regular
      expression</title>
      <para>
        A service routine used anywhere you want to match a regular
        expression at the start of a string.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e R e

def parseRe ( s, regex, message ):
    """Parse a regular expression at the head of a string.

      [ (s is a string) and
        (regex is a compiled regular expression) and
        (message is a string describing what is expected) ->
          if  s starts with a string that matches regex ->
            return (head, tail) where head is the part of s
            that matched and tail is the rest
          else ->
            raise SyntaxError, "Expecting (message)" ]
    """

    #-- 1 --
    # [ if the head of s matches regex ->
    #     m  :=  a match object describing the matching part
    #   else -> raise SyntaxError, "Expecting (message)" ]
    m  =  regex.match ( s )
    if  m is None:
        raise SyntaxError, "Expecting %s: '%s'" % (message, s)
</programlisting>
      <para>
        The <code >match</code > instance <code >m</code > has a
        method <code >.group()</code > that returns the matched text.
        The <code >m.end()</code > method returns the position just
        past the matched text.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 2 --
    # [ return (matched text from s, text from s after match) ]
    head  =  m.group()
    tail  =  s[m.end():]
    return  (head, tail)
</programlisting>
    </section> <!--parseRe-->
    <section id='parseLat'>
      <title><code >parseLat()</code >: Convert an external
      latitude</title>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e L a t

def parseLat ( s ):
    """Validate and convert an external latitude.

      [ s is a nonempty string ->
          if s is a valid external latitude ->
            return that latitude in radians
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        A latitude has two pieces: an angle, and a suffix letter
        which must be either &#x201c;<code >n</code >&#x201d; or
        &#x201c;<code >s</code >&#x201d;, case-insensitive.
        First we'll peel off the suffix and check it using
        <xref linkend='NS_PAT' />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ last  :=  last character of s
    #   rawAngle  :=  s up to the last character ]
    last  =  s[-1]
    rawAngle  =  s[:-1]

    #-- 2 --
    # [ if last matches NS_PAT ->
    #     nsFlag  :=  last, lowercased
    #   else -> raise SyntaxError ]
    m  =  NS_PAT.match ( last )
    if  m is None:
        raise SyntaxError, ( "Latitude '%s' does not end with 'n' "
                             "or 's'." % s )
    else:
        nsFlag  =  last.lower()
</programlisting>
      <para>
        Validating the angle part is done by <xref
        linkend='parseAngle' />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 3 --
    # [ if rawAngle is a valid angle ->
    #     absAngle  :=  that angle in radians
    #   else -> raise SyntaxError ]
    absAngle  =  parseAngle ( rawAngle )
</programlisting>
      <para>
        North latitude is positive, and south latitude is
        negative.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 4 --
    if  nsFlag == 's':  angle  =  - absAngle
    else:               angle  =  absAngle

    #-- 5 --
    return angle
</programlisting>
    </section> <!--parseLat-->
    <section id='parseLon'>
      <title><code >parseLon()</code >: Convert an external
      longitude</title>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e L o n

def parseLon ( s ):
    """Validate and convert an external longitude.

      [ s is a nonempty string ->
          if s is a valid external longitude ->
            return that longitude in radians
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        First we'll use <xref linkend='EW_PAT' /> to validate the
        suffix letter specifying east or west longitude.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    # [ last  :=  last character of s
    #   rawAngle  :=  s up to the last character ]
    last  =  s[-1]
    rawAngle  =  s[:-1]

    #-- 2 --
    # [ if EW_PAT matches last ->
    #     ewFlag  :=  last, lowercased
    #   else -> raise SyntaxError ]
    m  =  EW_PAT.match ( last )
    if  m is None:
        raise SyntaxError, ( "Longitude '%s' does not end with "
                             "'e' or 'w'." % s )
    else:
        ewFlag  =  last.lower()
</programlisting>
      <para>
        Next, check the angle with <xref linkend='parseAngle' />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 3 --
    # [ if rawAngle is a valid angle ->
    #     absAngle  :=  that angle in radians
    #   else -> raise SyntaxError ]
    absAngle  =  parseAngle ( rawAngle )
</programlisting>
      <para>
        All that remains is to attach the sign.  West Longitude
        is converted to East Longitude by subtracting from
        2&#x03c0;.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 4 --
    if  ewFlag == 'w':   angle  =  TWO_PI - absAngle
    else:                angle  =  absAngle

    #-- 5 --
    return  angle
</programlisting>
    </section> <!--parseLon-->
    <section id='parseHours'>
      <title><code >parseHours()</code >: Convert an external
      quantity in hours</title>
      <programlisting role='outFile:&spy;'
># - - -   p a r s e H o u r s

def parseHours ( s ):
    """Validate and convert a quantity in hours.

      [ s is a non-empty string ->
          if s is a valid mixed hours expression ->
            return the value of s as decimal hours
          else -> raise SyntaxError ]
    """
</programlisting>
      <para>
        First we'll set the optional <code >minute</code > and
        <code >second</code > to their default values of zero.
        The only required part is a floating hours followed by
        &#x201c;<code >h</code >&#x201d; (case-insensitive).
        We'll use <xref linkend='parseFloatSuffix' /> to process
        both the float and the suffix letter.  The process
        is repeated for the minutes and seconds if present.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 1 --
    minute  =  second  =  0.0

    #-- 2 --
    # [ if s starts with a float followed by 'h' or 'H' ->
    #     hour  :=  that float as type float
    #     minTail  :=  s after that float and suffix
    #   else -> raise SyntaxError ]
    hour, minTail  =  parseFloatSuffix ( s, H_PAT,
                          "Hours followed by 'h'" )

    #-- 3 --
    # [ if minTail is empty -> I
    #   else if minTail has the form "(float)m" ->
    #     minute  :=  that (float)
    #   else if minTail has the form "(float)m(float)s" ->
    #     minute  :=  the first (float)
    #     second  :=  the second (float)
    #   else -> raise SyntaxError ]
    if  len(minTail) != 0:
        #-- 3.1 --
        # [ if minTail starts with a float followed by 'm' or 'M' ->
        #     minute  :=  that float as type float
        #     secTail  :=  minTail after all that
        #   else -> raise SyntaxError ]
        minute, secTail  =  parseFloatSuffix ( minTail, M_PAT,
                                "Minutes followed by 'm'" )

        #-- 3.2 --
        # [ if secTail is empty -> I
        #   else if secTail starts with a float followed by
        #   's' or 'S' ->
        #     second  :=  that float as type float
        #     checkTail  :=  secTail after all that
        #   else -> raise SyntaxError ]
        if  len(secTail) != 0:
            second, checkTail  =  parseFloatSuffix ( secTail,
                S_PAT, "Seconds followed by 's'" )
            if  len(checkTail) != 0:
                raise SyntaxError, ( "Unidentifiable angle parts: "
                                     "'%s'" % checkTail )
</programlisting>
      <para>
        To convert from mixed units, we use <xref linkend='dmsUnits'
        />.
      </para>
      <programlisting role='outFile:&spy;'
>    #-- 4 --
    # [ return the quantity (hour, minute, second) in hours ]
    result  =  dmsUnits.mixToSingle ( (hour, minute, second) )
    return  result
</programlisting>
    </section> <!--parseHours-->
  </section> <!--functions-->
  <section id='class-MixedUnits'>
    <title><code >class MixedUnits</code >: Operations on
    mixed-unit systems</title>
    <programlisting role='outFile:&spy;'>
# - - - - -   c l a s s   M i x e d U n i t s

class MixedUnits:
    """Represents a system with mixed units, e.g., hours/minutes/seconds
    """
</programlisting>
    <section id='MixedUnits-init'>
      <title><code >MixedUnits.__init__()</code >: Constructor</title>
      <para>
        The constructor has little to do except store its
        argument in the instance.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   M i x e d U n i t s . _ _ i n i t _ _

    def __init__ ( self, factors ):
        """Constructor
        """
        self.factors  =  factors
</programlisting>
    </section> <!--MixedUnits-init-->
    <section id='MixedUnits-mixToSingle'>
      <title><code >MixedUnits.mixToSingle()</code >: Convert to
      a single value</title>
      <para>
        This method converts a value in mixed units to a single
        value in the largest unit.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   M i x e d U n i t s . m i x T o S i n g l e

    def mixToSingle ( self, coeffs ):
        """Convert mixed units to a single value.

          [ coeffs is a sequence of numbers not longer than
            len(self.factors)+1 ->
              return the equivalent single value in self's system ]
        """
</programlisting>
      <para>
        First let's work out the math.  Consider the
        days-hours-minutes-seconds system: the factor list is
        (24, 60, 60).  So there are 24 hours in a day;
        24&#x00d7;60 or 1440 minutes in a day; and
        24&#x00d7;60&#x00d7;60 or 86,400 seconds in a day.
      </para>
      <para>
        The formula we use for conversion depends on how many
        elements are in the <code >coeffs</code > sequence.  If
        there is only one value, it is treated as days.  Two
        values are treated as days and hours; three values are
        treated as days, hours, and minutes; and so on.  Here are
        the equations for converting <code >coeffs</code > values
        of one, two, three, and four elements, respectively:
      </para>
      <informalequation>
        <!--Source file is t0-1.tex.
         !-->
        <mediaobject>
          <imageobject role="html">
            <imagedata fileref="t0-1.jpg"/>
          </imageobject>
          <imageobject role="fo">
            <imagedata fileref="t0-1.pdf"/>
          </imageobject>
       </mediaobject>
      </informalequation>
      <para>
        For example, the expression for reducing 38&#x00b0; 52&#x2032;
        30.7&#x2033; to decimal degrees is <code
        >((30.7/60)+52)/60+38</code > or about 38.875194 degrees.
      </para>
      <para>
        Note the way we rewrite the expressions to form a
        sequence of alternating divide and add operations,
        starting with the smallest units and working toward the
        largest.  This suggests the form of the evaluation loop:
        we will work through the factor list from right to left,
        adding to the total a value from <code >coeffs</code >,
        and then dividing the total by the corresponding factor
        in turn.  Finally we add the first element of <code
        >coeffs</code >, unweighted.
      </para>
      <para>
        In order to simplify the logic that matches elements of
        <code >coeffs</code > to their corresponding elements of
        the factor list, if <code >coeffs</code > is shorter than
        the maximum length, we'll make a copy of <code
        >coeffs</code > with zero elements added on the right if
        necessary to pad it to the standard length.
        See <xref linkend='MixedUnits-pad' />, which may raise <code
        >ValueError</code > if there are too many elements.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        total  =  0.0

        #-- 2 --
        # [ if  len(coeffs) &lt;= len(self.factors)+1 ->
        #     coeffList  :=  a copy of coeffs, right-padded to length
        #         len(self.factors)+1 with zeroes if necessary ]
        coeffList  =  self.__pad ( coeffs )
</programlisting>
      <para>
        At this point, we use Python's negative indexing
        convention to work through the coefficients from right to
        left, adding each coefficient, then dividing by the
        factor list element at the same position.  The <code
        >range()</code > expression here generates the sequence
        <code >[-1, -2, &#x2026;, -len(self.factors)]</code >.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        # [ total  +:=  (coeffList[-1] * 
        #        (product of all elements of self.factors)) +
        #       (coeffList[-2] *
        #        (product of all elements of self.factors[:-1])) +
        #       (coeffList[-3] *
        #        (product of all elements of self.factors[:-2]))
        #        ... ]
        for  i in range ( -1, -len(self.factors)-1, -1):
            total  +=  coeffList[i]
            total  /=  self.factors[i]
</programlisting>
      <para>
        That takes care of all elements of <code >coeffList</code
        > but the first; we add that one in, unweighted.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 4 --
        total  +=  coeffList[0]

        #-- 5 --
        return total
</programlisting>
    </section> <!--MixedUnits-mixToSingle-->
    <section id='MixedUnits-pad'>
      <title><code >MixedUnits.__pad()</code >: Pad short
      coefficient lists to standard length</title>
      <para>
        This utility method is used to make a copy of a coefficient
        list, and add zeroes on the right if necessary to pad it out
        to the standard length, which is <code
        >len(self.factors)+1</code >.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   M i x e d U n i t s . _ _ p a d

    def __pad ( self, coeffs ):
        """Pad coefficient lists to standard length.

          [ coeffs is a sequence of numbers ->
              if  len(coeffs) > len(self.factors)+1 ->
                raise ValueError
              else ->
                return a list containing the elements of coeff,
                plus additional zeroes on the right if necessary
                so that the result has length len(self.factors)+1 ]
        """
</programlisting>
      <para>
        First we set <code >stdLen</code > to the standard length,
        then set <code >shortage</code > to the number of elements
        we have to add to <code >coeffs</code > to reach that
        length.  If <code >shortage</code > is negative, that's an
        error: <code >coeffs</code > is too long for this system.
      </para>
      <para>
        We use the fact that Python's <code >list()</code > function
        makes a copy of its argument, even if the argument is a
        list.  If the argument is a tuple, it is converted to a
        list.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ stdLen  :=  1 + len(self.factors)
        #   shortage  :=  1 + len(self.factors) - len(coeffs)
        #   result  :=  a copy of coeffs as a list ]
        stdLen  =  1 + len(self.factors)
        shortage  =  stdLen - len(coeffs)
        result  =  list(coeffs)

        #-- 2 --
        # [ if shortage &lt; 0 ->
        #     raise ValueError
        #   else ->
        #     result  :=  result + (a list of shortage zeroes) ]
        if  shortage &lt; 0:
            raise ValueError, ( "Value %s has too many elements; "
                "max is %d." % (coeffs, stdLen) )
        elif  shortage &gt; 0:
            result  +=  [0.0] * shortage

        #-- 3 --
        return result
</programlisting>
    </section> <!--MixedUnits-pad-->
    <section id='MixedUnits-singleToMix'>
      <title><code >MixedUnits.singleToMix()</code >: Convert to
      mixed units</title>
      <para>
        This method takes a single quantity, in the largest unit
        of the system, and converts it to mixed units.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   M i x e d U n i t s . s i n g l e T o M i x

    def singleToMix ( self, value ):
        """Convert to mixed units.

          [ value is a float ->
              return value as a sequence of coefficients in
              self's system ]
        """
</programlisting>
      <para>
        The first element of the <code >result</code > list is
        the whole part of the value.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ whole  :=  whole part of value
        #   frac  :=  fractional part of value ]
        whole, frac  =  divmod ( value, 1.0 )
        result  =  [int(whole)]
</programlisting>
      <para>
        For each factor in the system, we then multiply the
        fraction from the previous step by that factor, and
        separate the result into whole and fractional parts.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 2 --
        # [ result  :=  result with integral parts of value
        #               in self's system appended ]
        for  factorx in range(len(self.factors)):
            frac  *=  self.factors[factorx]
            whole, frac  =  divmod ( frac, 1.0 )
            result.append ( int(whole) )
</programlisting>
      <para>
        After the loop above, <code >frac</code > contains the
        fractional part of the value in the smallest unit, so we
        must add it to the last element of the result.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        # [ result  :=  result with frac added to its last element ]
        result[-1]  +=  frac

        #-- 4 --
        return result
</programlisting>
    </section> <!--MixedUnits-singleToMix-->
    <section id='MixedUnits-format'>
      <title><code >MixedUnits.format()</code >: Format mixed
      units</title>
      <para>
        This method takes a mixed-units value as a sequence of
        numbers, and returns a list of strings containing those
        numbers formatted in the way described in the <ulink
url='http://www.nmt.edu/tcc/help/lang/python/examples/sidereal/MixedUnits-format.html'
        >specification</ulink >.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   M i x e d U n i t s . f o r m a t

    def format ( self, coeffs, decimals=0, lz=False ):
        """Format mixed units.

          [ (coeffs is a sequence of numbers as returned by
            MixedUnits.singleToMix()) and
            (decimals is a nonnegative integer) and
            (lz is a bool) ->
              return a list of strings corresponding to the values
              of coeffs, with all the values but the last formatted
              as integers, all values zero padded iff lz is true,
              and the last value with (decimals) digits after the
              decimal point ]
        """
</programlisting>
      <para>
        The important feature of this method is to prevent
        ever returning a last string which is greater than equal
        to <code >self.factors[-1]</code >.  For example, in
        the degrees-minutes-seconds system, if the seconds value
        is 59.9999, we want to avoid ever showing a value of
        <code >"60"</code >, <code >"60.0"</code >, or anything
        like that.
      </para>
      <para>
        We'll implement this constraint by truncating any extra
        digits, so 59.999 will display as <code >"59"</code >,
        <code >"59.9"</code >, <code >"59.99"</code >, and so
        forth for the various values of <code >decimals</code >.
      </para>
      <para>
        For the <code >decimals=0</code > case, if the fractional
        part of the last value is 0.5 or greater, formatting with
        <code >"%.0f"</code > will round the value undesirably;
        in that case, we subtract 0.5 from the last value to
        effectively truncate instead of rounding.
      </para>
      <para>
        For <code >decimals=1</code >, if the fractional part is
        0.95 or greater, formatting with <code >"%.1f"</code >
        will round.  In that case, we subtract 0.05 from
        the last value.
      </para>
      <para>
        Progressing toward a general case, here is a table
        showing the first few values of <code >decimals</code >,
        the range of fraction values that will cause rounding,
        and the fudge factor to be subtracted from the last value
        to prevent rounding.
      </para>
      <informaltable>
        <tgroup cols="3">
          <colspec align="center"/>
          <colspec align="left"/>
          <colspec align="left"/>
          <thead>
            <row>
              <entry><code >decimals</code ></entry>
              <entry>Rounding occurs</entry>
              <entry>Fudge factor</entry>
            </row>
          </thead>
          <tbody>
            <row>
              <entry>0</entry>
              <entry>&gt;= 0.5</entry>
              <entry>0.5</entry>
            </row>
            <row>
              <entry>1</entry>
              <entry>&gt;= 0.95</entry>
              <entry>0.05</entry>
            </row>
            <row>
              <entry>2</entry>
              <entry>&gt;= 0.995</entry>
              <entry>0.005</entry>
            </row>
            <row>
              <entry>3</entry>
              <entry>&gt;= 0.9995</entry>
              <entry>0.0005</entry>
            </row>
          </tbody>
        </tgroup>
      </informaltable>
      <para>
        Generalizing, whenever the fractional part of the last
        value in the <code >coeffs</code > sequence is greater
        than or equal to <replaceable
        >1-0.5&#x00d7;10<superscript >-decimals</superscript
        ></replaceable >, we must subtract <replaceable
        >0.5&#x00d7;10<superscript >-decimals</superscript
        ></replaceable > from the last value.
      </para>
      <para>
        First, we set <code >coeffList</code > to a copy of <code
        >coeffs</code >, right-padded with zero elements to standard
        length.  Then we use a list comprehension to format all the
        values but the last as integers.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        coeffList  =  self.__pad ( coeffs )

        #-- 2 --
        # [ result  :=  the values from coeffList[:-1] formatted
        #               as integers ]
        if  lz:  fmt = "%02d"
        else:    fmt = "%d"
        result  =  [ fmt % x
                     for x in coeffList[:-1] ]
</programlisting>
      <para>
        Next we separate the last value into whole and fractional
        parts, and set <code >fuzz</code > to the quantity 
        <replaceable
        >0.5&#x00d7;10<superscript >-decimals</superscript
        ></replaceable >.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 2 --
        # [ whole  :=  whole part of coeffList[-1]
        #   frac   :=  fractional part of coeffList[-1]
        #   fuzz   :=  0.5 * (10 ** (-decimals) ]
        whole, frac  =  divmod ( float(coeffList[-1]), 1.0 )
        fuzz = 0.5 * (10.0 ** (-decimals))
</programlisting>
      <para>
        Now apply the test and adjust if necessary.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        # [ if  frac >= (1-fuzz) ->
        #     result  +:=  [whole+frac-fuzz], formatted with
        #                  (decimals) digits after the decimal
        #   else ->
        #     result  +=   coeffList[-1], formatted with (decimals)
        #                  digits after the decimal ]
        if  frac >= (1.0-fuzz):
            corrected  =  whole + frac - fuzz
        else:
            corrected  =  coeffList[-1]
</programlisting>
      <para>
        Next, the formatting.  There are three cases.
      </para>
      <itemizedlist spacing="compact">
        <listitem>
          <para>
            If there are no left zeroes, we just use a format of the
            form <code >"%.<replaceable >D</replaceable >f"</code >,
            where <code ><replaceable >D</replaceable ></code > is
            <code >decimals</code >.
          </para>
        </listitem>
        <listitem>
          <para>
            If the caller wants left zeroes, the format has the form
            <code >"%0<replaceable >N</replaceable >.<replaceable
            >D</replaceable >f"</code >, where <code >N</code > is
            <code >decimals</code > plus three&#x2014;two for the
            digits to the left of the decimal, and one for the
            decimal.
          </para>
        </listitem>
        <listitem>
          <para>
            For left zeroes in the case of <code >decimals=0</code
            >, the format code is <code >"%0<replaceable
            >N</replaceable >.<replaceable >D</replaceable >f"</code
            >, but in that case <code ><replaceable >N</replaceable
            ></code > is one less, because there will be no decimal
            point.
          </para>
        </listitem>
      </itemizedlist>
      <para>
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 4 --
        # [ if lz ->
        #     s  :=  corrected, formatted with 2 digits of left-zero
        #            padding and (decimals) precision
        #   else ->
        #     s  :=  corrected, formatted with (decimals) precision ]
        if  lz:
            if  decimals:  n = decimals+3
            else:          n = decimals+2

            s  =  "%0*.*f" % (n, decimals, corrected)
        else:
            s  =  "%.*f" % (decimals, corrected)
              
        #-- 5 --
        result.append ( s )

        #-- 6 --
        return result
</programlisting>
    </section> <!--MixedUnits-format-->
  </section> <!--class-MixedUnits-->
  <section id='dmsUnits'>
    <title><code >dmsUnits</code >: Mixed-units converter</title>
    <para>
      This instance of <code >MixedUnits</code > is used to
      convert to and from mixed units in either the
      hours-minutes-seconds system or the degrees-minutes-seconds
      system.  Both systems have the same factor list: <code
      >(60, 60)</code >.
    </para>
    <programlisting role='outFile:&spy;'
>dmsUnits = MixedUnits ( (60, 60) )
</programlisting>
  </section> <!--dmsUnits-->
  <section id='class-LatLon'>
    <title><code >class LatLon</code >: Observer latitude and
    longitude</title>
    <para>
      An instance of this class represents a position on the
      earth's surface as a latitude (signed, positive for north
      of the equator) and a longitude (east from longitude
      0&#x00b0;).
    </para>
    <programlisting role='outFile:&spy;'>
# - - - - -   c l a s s   L a t L o n

class LatLon:
    """Represents a latitude+longitude.
    """
</programlisting>
    <section id='LatLon-init'>
      <title><code >LatLon.__init__()</code >: Constructor</title>
      <para>
        The constructor takes the values directly in radians, and
        stores them that way.  The longitude is normalized to
        the interval [0, 2&#x03c0;).
      </para>
      <programlisting role='outFile:&spy;'
># - - -  L a t L o n . _ _ i n i t _ _

    def __init__ ( self, lat, lon ):
        """Constructor for LatLon.
        """
        self.lat  =  lat
        self.lon  =  lon % TWO_PI
</programlisting>
    </section> <!--LatLon-init-->
    <section id='LatLon-str'>
      <title><code >LatLon.__str__()</code >: Convert to a
      string</title>
      <programlisting role='outFile:&spy;'
># - - -   L a t L o n . _ _ s t r _ _

    def __str__ ( self ):
        """Return self as a string.
        """
</programlisting>
      <para>
        Values returned here should look like this example:
      </para>
      <programlisting
>[03d 14m 32s N Lat 118d 32m 04s W Lon]
</programlisting>
      <para>
        Because <code >self.lon</code > is in the range [0,
        2&#x03c0;], we have to treat values in the range
        [&#x03c0;, 2&#x03c0;) differently: values in that range
        are subtracted from 2&#x03c0;, and those will be shown as
        west longitude.  Similarly, negative latitudes are shown
        as positive south latitude.  The <code >degrees()</code >
        function from Python's <code >math</code > module
        converts radians to degrees.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        if  self.lon >= pi:
            e_w  =  "W"
            lonDeg  =  degrees ( TWO_PI - self.lon )
        else:
            e_w  =  "E"
            lonDeg  =  degrees ( self.lon )

        #-- 2 --
        if  self.lat &lt; 0:
            n_s  =  "S"
            latDeg  =  degrees ( - self.lat )
        else:
            n_s  =  "N"
            latDeg  =  degrees ( self.lat )
</programlisting>
      <para>
        Next we get lists of formatted values in the
        degrees-minutes-seconds system.  The rest is just
        simple formatting.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        # [ latList  :=  three formatted values of latDeg in
        #                degrees/minutes/seconds
        #   lonList  :=  three formatted values of lonDeg similarly ]
        latList  =  dmsUnits.format ( dmsUnits.singleToMix(latDeg), 1 )
        lonList  =  dmsUnits.format ( dmsUnits.singleToMix(lonDeg), 1 )

        #-- 4 --
        return ( '[%sd %s\' %s" %s Lat  %sd %s\' %s" %s Lon]' %
                 (latList[0], latList[1], latList[2], n_s,
                  lonList[0], lonList[1], lonList[2], e_w) )
</programlisting>
    </section> <!--LatLon-str-->
  </section> <!--class-LatLon-->
  <section id='class-JulianDate'>
    <title><code >class JulianDate</code >: Julian calendar
    timestamp</title>
    <para>
      For a discussion of the Julian date system, see <ulink
      url='http://www.nmt.edu/tcc/help/lang/python/examples/sidereal/class-JulianDate.html'
      >the specification</ulink >.
    </para>
    <programlisting role='outFile:&spy;'>
# - - - - -   c l a s s   J u l i a n D a t e

class JulianDate:
    """Class to represent Julian-date timestamps.

      State/Invariants:
        .f:  [ (Julian date as a float) - JULIAN_BIAS ]
    """
</programlisting>
    <section id='JulianDate-init'>
      <title><code >JulianDate.__init__()</code >: Constructor</title>
      <para>
        The value represented is the some of the two arguments
        (the second of which default to zero), but <code
        >JULIAN_BIAS</code > is subtracted before the two
        argument values are added, to avoid loss of significance.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   J u l i a n D a t e . _ _ i n i t _ _

    def __init__ ( self, j, f=0.0 ):
        """Constructor for JulianDate.
        """
        self.j  =  j - JULIAN_BIAS + f
</programlisting>
    </section> <!--JulianDate-init-->
    <section id='JulianDate-float'>
      <title><code >JulianDate.__float__()</code >: Convert to a
      float</title>
      <para>
        This method adds <code >JULIAN_BIAS</code > to the
        internal value so that it is the standard value.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   J u l i a n D a t e . _ _ f l o a t _ _

    def __float__ ( self ):
        """Convert self to a float.
        """
        return  self.j + JULIAN_BIAS
</programlisting>
    </section> <!--JulianDate-float-->
    <section id='JulianDate-datetime'>
      <title><code >JulianDate.datetime()</code >: Convert to a
      <code >datetime</code ></title>
      <para>
        Converts a <code >JulianDate</code > instance to a Python
        <code >datetime</code > value.  The algorithm here is
        rather inscrutable; refer to <link linkend='references'
        >Duffett-Smith</link > for the details.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   J u l i a n D a t e . d a t e t i m e

    def datetime ( self ):
        """Convert to a standard Python datetime object in UT.
        """
</programlisting>
      <para>
        Step 1 of the algorithm reads: &#x201c;Add 0.5 to JD. Set
        I=integer part and F=fractional part.&#x201d; Once we
        have extracted the fractional days part, we can safely
        add the <code >JULIAN_BIAS</code > back in with no loss
        of significance.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ i  :=  int(self.j + 0.5)
        #   f  :=  (self.j + 0.5) % 1.0 ]
        i, f  =  divmod ( self.j + 0.5, 1.0 )
        i  +=  JULIAN_BIAS
</programlisting>
      <para>
        Step 2: &#x201c;If I is larger than 2,299,160, calculate
        A = integer part of ((I- 1,867,216.25) / 36,524.25);
        calculate B = I + 1 + a - integer part of (A/4);
        otherwise set A = I.&#x201d; The last part of this
        sentence should probably set &#x201c;...otherwise set B =
        I,&#x201d; since A is not referenced after this step, but
        B certainly is.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 2 --
        if  i > 2299160:
            a  =  int((i-1867216.25)/36524.25)
            b  =  i + 1 + a - int ( a / 4.0 )
        else:
            b  =  i
</programlisting>
      <para>
        Step 3: &#x201c;Calculate C = B + 1524.&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        c = b + 1524
</programlisting>
      <para>
        Step 4: &#x201c;Calculate D = integer part of
        (C-122.1)/365.25.&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 4 --
        d = int((c-122.1)/365.25)
</programlisting>
      <para>
        Step 5: &#x201c;Calculate E = integer part of (365.25
        &#x00d7; D ).&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 5 --
        e = int(365.25*d)
</programlisting>
      <para>
        Step 6: &#x201c;Calculate G = integer part of
        ((C-E)/30.6001).&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 6 --
        g = int((c-e)/30.6001)
</programlisting>
      <para>
        Step 7: &#x201c;Calculate d=C-E+F-integer part of
        (30.6001 &#x00d7; G).  This is the day of the month,
        including the decimal fraction of the day.&#x201d; We
        will remove the fractional day and convert it to hours,
        minutes, and seconds.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 7 --
        dayFrac = c - e + f - int ( 30.6001 * g )
        day, frac = divmod ( dayFrac, 1.0 )
        dd = int(day)
        hr, mn, sc = dmsUnits.singleToMix ( 24.0*frac )
</programlisting>
      <para>
        Step 8: &#x201c;Calculate m=G-1 if G is less than
        13.5, or m=G-13 if G is more than 13.5.  This is the month
        number.&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 8 --
        if  g &lt; 13.5:  mm = int(g - 1)
        else:             mm = int(g - 13)
</programlisting>
      <para>
        Step 9: &#x201c;Calculate y = D-4716 if m is more
        than 2.5, or y = D - 4715 if m is less than 2.5.  This is
        the year.&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 9 --
        if  mm > 2.5:  yyyy = int(d-4716)
        else:          yyyy = int(d-4715)
</programlisting>
      <para>
        Now that we have all the pieces, assemble them into a
        <code >datetime</code > and return it.  One further
        elaboration: the value of <code >sc</code > is a float,
        and the <code >datetime.datetime()</code > constructor
        wants fractional seconds passed as a
        &#x201c;microseconds&#x201d; argument.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 10 --
        sec, fracSec = divmod(sc, 1.0)
        usec = int(fracSec * 1e6)
        return datetime.datetime ( yyyy, mm, dd, hr, mn, int(sec),
                                   usec )
</programlisting>
    </section> <!--JulianDate-datetime-->
    <section id='JulianDate-offset'>
      <title><code >JulianDate.offset()</code >: Move a time by
      some number of days</title>
      <para>
        For a <code >JulianDate</code > instance <code
        ><replaceable >JD</replaceable ></code >, this method
        returns a new <code >JulianDate</code > instance that
        differs by <code >delta</code > days.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   J u l i a n D a t e . o f f s e t

    def offset ( self, delta ):
        """Return a new JulianDate for self+(delta days)

          [ delta is a number of days as a float ->
              return a new JulianDate (delta) days in the
              future, or past if negative ]
        """
</programlisting>
      <para>
        To preserve the precision, we'll perform the addition
        directly on the internal value that is biased by <code
        >JULIAN_BIAS</code >, then separate the sum into whole
        part and fraction, and then un-bias the whole part and
        pass both to the class constructor.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        newJ  =  self.j + delta

        #-- 2 --
        # [ newWhole  :=  whole part of newJ
        #   newFrac   :=  fractional part of newJ ]
        newWhole, newFrac  =  divmod ( newJ )

        #-- 3 --
        return  JulianDate ( newWhole+JULIAN_BIAS, newFrac )
</programlisting>
    </section> <!--JulianDate-offset-->
    <section id='JulianDate-sub'>
      <title><code >JulianDate.__sub__()</code >: Difference
      of two Julian dates</title>
      <para>
        The <code >other</code > must be a <code
        >JulianDate</code > instance.  The return value is the
        difference in days.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   J u l i a n D a t e . _ _ s u b _ _

    def __sub__ ( self, other ):
        """Implement subtraction.

          [ other is a JulianDate instance ->
              return self.j - other.j ]
        """
        return  self.j - other.j
</programlisting>
    </section> <!--JulianDate-sub-->
    <section id='JulianDate-cmp'>
      <title><code >JulianDate.__cmp__()</code >: Compare two
      Julian dates</title>
      <para>
        The ordinary Python comparison function.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   J u l i a n D a t e . _ _ c m p _ _

    def __cmp__ ( self, other ):
        """Compare two instances.

          [ other is a JulianDate instance ->
              if  self.j &lt; other.j ->  return a negative number
              else if self.j == other.j -> return zero
              else -> return a positive number ]
        """
        return  cmp ( self.j, other.j )
</programlisting>
    </section> <!--JulianDate-cmp-->
    <section id='JulianDate-fromDatetime'>
      <title><code >JulianDate.fromDatetime()</code >: Convert a
      <code >datetime</code > to a Julian date</title>
      <para>
        This static method converts a <code >JulianDate</code >
        instance <code ><replaceable >JD</replaceable ></code >
        to one of Python's regular <code >datetime</code >
        objects.  In <link linkend='references'
        >Duffett-Smith</link >, this is section 4.
      </para>
      <para>
        This method handles both &#x201c;naive&#x201d; and
        &#x201c;aware&#x201d; <code >datetime</code > instances.
        You may want to review these concepts in the <ulink
        url='http://docs.python.org/lib/module-datetime.html'
        >online documentation for <code >datetime</code ></ulink
        >.  A naive instance is assumed to represent UTC.  For an
        aware instance, we will use its <code >.utcoffset()</code
        > method to convert it to UTC.        
      </para>
      <programlisting role='outFile:&spy;'
># - - -   J u l i a n D a t e . f r o m D a t e t i m e

#   @staticmethod
    def fromDatetime ( dt ):
        """Create a JulianDate instance from a datetime.datetime.

          [ dt is a datetime.datetime instance ->
              if  dt is naive ->
                return the equivalent new JulianDate instance,
                assuming dt expresses UTC
              else ->
                return a new JulianDate instance for the UTC
                time equivalent to dt ]              
        """
</programlisting>
      <para>
        The first step is to derive the UTC equivalent to <code
        >dt</code >.  The documentation states that the return
        value from <code
        >.utcoffset()</code > is one of:
      </para>
      <itemizedlist spacing="compact">
        <listitem>
          <para>
            <code >None</code >, if the instance is naive.
          </para>
        </listitem>
        <listitem>
          <para>
            A <code >datetime.timedelta</code > instance
            representing the offset in hours east of London.  This
            value must be subtracted from a local time to get UTC.
            Note that subtracting a <code >timedelta</code >
            instance from a <code >datetime</code > instance
            yields a new <code >datetime</code > instance.
          </para>
        </listitem>
      </itemizedlist>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ if dt is naive ->
        #     utc  :=  dt
        #   else ->
        #     utc  :=  dt - dt.utcoffset() ]
        utc  =  dt
        offset  =  dt.utcoffset()
        if  offset:
            utc  =  dt - offset
</programlisting>
      <para>
        We synthesize a fractional day from the hours,
        minutes, seconds, and microseconds attributes.
        For the conversion of hours/minutes/seconds to hours, see
        <xref linkend='MixedUnits-mixToSingle' />; the resulting
        hours are divided by 24.0 to get days.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 2 --
        # [ fracDay  :=  fraction of a day in [0.0,1.0) made from
        #       utc.hour, utc.minute, utc.second, and utc.microsecond ]
        s  =  float(utc.second) + float(utc.microsecond)*1e-6
        hours  =  dmsUnits.mixToSingle ( (utc.hour, utc.minute, s) )
        fracDay  =  hours / 24.0
</programlisting>
      <para>
        We extract the year, month, and day from <code >utc</code >.
        This is Step 1 from the book.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        y  =  utc.year
        m  =  utc.month
        d  =  utc.day
</programlisting>
      <para>
        Step 2: &#x201c;If m=1 or m=2 subtract 1 from y and add
        12 to m.  Otherwise y'=y and m'=m.&#x201d; Rather than
        having separate variables for y' and m', we'll just
        modify variables <code >y</code > and <code >d</code > in
        place.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 4 --
        if  m &lt;= 2:
            y, m  =  y-1, m+12
</programlisting>
      <para>
        Step 3: &#x201c;If the date is later than 1582 October 15
        (i.e., Gregorian calendar) calculate:
      </para>
      <itemizedlist spacing="compact">
        <listitem>
          <para>
            (i) A = integer part of (y'/100);
          </para>
        </listitem>
        <listitem>
          <para>
            (ii) B = 2 - A + integer part of (A/4).
          </para>
        </listitem>
      </itemizedlist>
      <para>
        Otherwise B = 0.&#x201d;  Note how easy it is to compare
        dates in mixed units: Python defines ordering on tuples
        in the correct way.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 5 --
        if  ( (y, m, d) &gt;= (1582, 10, 15) ):
            A  =  int ( y / 100 )
            B  =  2 - A + int ( A / 4 )
        else:
            B  =  0
</programlisting>
      <para>
        Step 4: &#x201c;Calculate C = integer part of
        (365.25&#x00d7;y').&#x201d;  Step 5: &#x201c;Calculate D =
        integer part of (30.6001&#x00d7;(m'+1)).&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 6 --
        C  =  int ( 365.25 * y )
        D  =  int ( 30.6001 * ( m + 1 ) )
</programlisting>
      <para>
        Step 6: &#x201c;Find JD = B + C + D + d + 1 720 994.5.
        This is the Julian date.&#x201d; To preserve extra
        precision over a purely <code >float</code >
        representation of the Julian date, we will do the
        addition of the last constant in multiple steps.  First
        we'll add the 0.5 fractional part to <code >fracDay</code
        > and, if that sum exceeds 1.0, add the carry to the day
        number <code >d</code >, retaining the fractional part in
        <code >fracDay</code >.  Then we'll add the remaining
        parts together to get the whole part.
      </para>
       <programlisting role='outFile:&spy;'
>        #-- 7 --
        # [ if fracDay+0.5 >= 1.0 ->
        #     s  +=  1
        #     fracDay  :=  (fracDay+0.5) % 1.0
        #   else ->
        #     fracDay  :=  fracDay + 0.5 ]
        dayCarry, fracDay  =  divmod ( fracDay+0.5, 1.0 )
        d  +=  dayCarry

        #-- 8 --
        j  =  B + C + D + d + 1720994

        #-- 9 --
        return  JulianDate ( j, fracDay )
</programlisting>
      <para>
        This next line is necessary to designate this method as a
        static method.
      </para>
      <programlisting role='outFile:&spy;'
>    fromDatetime = staticmethod(fromDatetime)
</programlisting>
    </section> <!--JulianDate-fromDatetime-->
  </section> <!--class-JulianDate-->
  <section id='class-SiderealTime'>
    <title><code >class SiderealTime</code >: Sidereal time</title>
    <para>
      An instance of this class represents a plain sidereal time
      in the range [0,24) hours.  No attempt is made inside the
      object to record whether it represents Greenwich sidereal
      time (GST) or local sidereal time (LST).
    </para>
    <para>
      Although the constructor accepts a value in hours,
      internally the value is stored as radians normalized to the
      interval [0,2&#x03c0;).
    </para>
    <programlisting role='outFile:&spy;'>
# - - - - -   c l a s s   S i d e r e a l T i m e

class SiderealTime:
    """Represents a sidereal time value.

      State/Internals:
        .hours:     [ self as 15-degree hours ]
        .radians:   [ self as radians ]
    """
</programlisting>
    <section id='SiderealTime-init '>
      <title><code >SiderealTime.__init__()</code >:
      Constructor</title>
      <para>
        The time in hours is first normalized to the interval
        [0,24) and stored in <code >self.hours</code >, then
        converted to radians and stored in <code
        >self.radians</code >.        
      </para>
      <programlisting role='outFile:&spy;'
># - - -   S i d e r e a l T i m e . _ _ i n i t _ _

    def __init__ ( self, hours ):
        """Constructor for SiderealTime
        """
        self.hours  =  hours % 24.0
        self.radians  =  hoursToRadians ( self.hours )
</programlisting>
    </section> <!--SiderealTime-init-->
    <section id='SiderealTime-str'>
      <title><code >SiderealTime.__str__()</code >: Convert to
      string</title>
      <para>
        To convert from decimal hours to mixed units and format
        them with left zero, we use <xref
        linkend='MixedUnits-singleToMix' />.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   S i d e r e a l T i m e . _ _ s t r _ _

    def __str__ ( self ):
        """Convert to a string such as "[04h 40m 5.170s]".
        """

        #-- 1 --
        # [ values  :=  self.hours as a list of mixed units
        #       in dmsUnits terms, formatted as left-zero
        #       filled strings with 3 digits after the decimal ]
        mix  =  dmsUnits.singleToMix ( self.hours )
        values  =  dmsUnits.format ( mix, decimals=3, lz=True )

        #-- 2 --
        return "[%sh %sm %ss]" % tuple(values)
</programlisting>
    </section> <!--SiderealTime-str-->
    <section id='SiderealTime-utc'>
      <title><code >SiderealTime.utc()</code >: Find Universal
      Time</title>
      <para>
        This method finds the first or only time on a given date
        when self's Greenwich Sidereal Time (GST) occurs.  The
        date is provided as a <code >datetime.date</code >
        instance.  In <link linkend='references'
        >Duffett-Smith</link >, this is section 13.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   S i d e r e a l T i m e . u t c

    def utc ( self, date ):
        """Convert GST to UTC.

          [ date is a UTC date as a datetime.date instance ->
              return the first or only time at which self's GST
              occurs at longitude 0 ]
        """
</programlisting>
      <para>
         &#x201c;Step 1: Find the number of days between January
         0.0 and the date in question.&#x201d; See <xref
         linkend='dayNo' />.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ nDays  :=  number of days between Jan. 0 of year
        #       (date.year) and date ]
        nDays  =  dayNo ( date )
</programlisting>
      <para>
        Step 2 is to multiply that by constant A, which in this
        script is defined in <xref linkend='SIDEREAL_A' />.  Step
        3 is to subtract factor B, which is a function of the
        year number; for the computation of this factor, see
        <xref linkend='SiderealTime-factorB' />.  Normalize the
        result of step 3 to range [0,24).  This is factor
        T<subscript >0</subscript >.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 2 --
        # [ t0  :=  (nDays * A - B(date.year)), normalized to
        #           interval [0,24) ]
        t0  =  ( ( nDays * SIDEREAL_A -
                   SiderealTime.factorB ( date.year ) ) % 24.0 )
</programlisting>
      <para>
        Step 4 is to convert the GST to decimal hours. Step 5 is
        subtract T<subscript >0</subscript >, and renormalize to
        the interval [0,24).
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        # [ t1  :=  ((self in decimal hours)-t0), normalized to
        #           the interval [0,24) ]
        t1  =  ( radiansToHours ( self.radians ) - t0 ) % 24.0
</programlisting>
      <para>
        Step 6: &#x201c;Multiply by constant D (0.997 270).  This
        is the GMT in hours.&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 4 --
        gmtHours  =  t1 * 0.997270
</programlisting>
      <para>
        The result is to be expressed as a <code
        >datetime.datetime</code > instance.  The date portion of
        this result is just a copy of the <code >date</code >
        argument.  We'll use <code >dmsUnits</code > (see <xref
        linkend='dmsUnits' />) to convert whole hours to hours,
        minutes, and seconds, then break out the microseconds as
        required by <code >datetime.datetime</code >.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 5 --
        # [ dt  :=  a datetime.datetime instance whose date comes
        #           from (date) and whose time is (gmtHours)
        #           decimal hours ]
        hour, minute, floatSec  =  dmsUnits.singleToMix ( gmtHours )
        wholeSec, fracSec  =  divmod ( floatSec, 1.0 )
        second  =  int ( wholeSec )
        micros  =  int ( fracSec * 1e6 )
        dt  =  datetime.datetime ( date.year, date.month,
                   date.day, hour, minute, second, micros )
        
        #-- 6 --
        return  dt
</programlisting>
    </section> <!--SiderealTime-utc-->
    <section id='SiderealTime-factorB'>
      <title><code >SiderealTime.factorB()</code >: Compute
      sidereal time factor B (static method)</title>
      <para>
        The number B is used in <xref linkend='SiderealTime-utc'
        /> and <xref linkend='SiderealTime-fromDatetime' />.  The
        algorith is given in section 12 of <link
        linkend='references' >Duffett-Smith</link >.  It is a
        function of the year, and expresses the GST at 0:00 on
        January 0 of the year.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   S i d e r e a l T i m e . f a c t o r B

#   @staticmethod
    def factorB ( yyyy ):
        """Compute sidereal conversion factor B for a given year.

          [ yyyy is a year number as an int ->
              return the GST at time yyyy-01-00T00:00 ]
        """
</programlisting>
      <para>
        Step 1: &#x201c;Calculate the Julian date of January 0.0
        of year in question.&#x201d; Unfortunately, we can't just
        blithely pass the <code >datetime.datetime()</code >
        constructor that date, because it's not a proper date
        (&#x201c;ValueError: day is out of range for
        month&#x201d;).  So we'll get around that by finding the
        JD of Jan. 1, converting it to a float, and subtracting 1.0.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ janJD  :=  the Julian date of January 0.0 of year
        #              (yyyy), as a float ]
        janDT  =  datetime.datetime ( yyyy, 1, 1 )
        janJD  =  float(JulianDate.fromDatetime(janDT)) - 1.0
</programlisting>
      <para>
        Steps 2&#x2013;4 are exactly as described in <link
        linkend='references' >Duffett-Smith</link >, except that
        we rearrange the polynomial (6.646 065 6+(2400.051
        262&#x00d7;T)+(0.000 025 81&#x00d7;T<superscript
        >2</superscript >) to the computationally more
        straightforward form.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 2 --
        s  =  janJD - 2415020.0

        #-- 3 --
        t  =  s / 36525.0

        #-- 4 --
        r  =  ( 0.00002581 * t +
                2400.051262 ) * t + 6.6460656
</programlisting>
      <para>
        Step 5: &#x201c;Calculate U=R-(24&#x00d7;(year-1900)).&#x201d;
        Step 6: &#x201c;Subtract U from 24.  This is B.&#x201d;
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 5 --
        u = r - 24 * ( yyyy-1900)

        #-- 6 --
        return 24.0 - u

    factorB = staticmethod(factorB)
</programlisting>
    </section> <!--SiderealTime-factorB-->
    <section id='SiderealTime-gst'>
      <title><code >SiderealTime.gst()</code >: Local to
      Greenwich sidereal</title>
      <para>
        In <link linkend='references' >Duffett-Smith</link >,
        this is section 15.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   S i d e r e a l T i m e . g s t

    def gst ( self, eLong ):
        """Convert LST to GST.

          [ self is local sidereal time at longitude eLong
            radians east of Greenwich ->
              return the equivalent GST as a SiderealTime instance ]
        """
</programlisting>
      <para>
        If we convert the longitude <code >eLong</code > to
        hours, this function becomes a simple matter of
        subtracting the longitude from the LST and normalizing to
        the interval [0,24).
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ deltaHours  :=  eLong expressed in hours ]
        deltaHours  =  radiansToHours ( eLong )

        #-- 2 --
        gstHours  =  ( self.hours - deltaHours ) % 24.0

        #-- 3 --
        return SiderealTime ( gstHours )
</programlisting>
    </section> <!--SiderealTime-gst-->
    <section id='SiderealTime-lst'>
      <title><code >SiderealTime.lst()</code >: Greenwich to
      local sidereal</title>
      <para>
        This is the inverse of <xref linkend='SiderealTime-gst'
        />: <code >self</code > is assumed to be GST, and we want
        to find the equivalent LST.  All we have to do is add the
        longitude to the GST and renormalize to [0,24).
      </para>
      <programlisting role='outFile:&spy;'
># - - -   S i d e r e a l T i m e . l s t

    def lst ( self, eLong ):
        """Convert GST to LST.

          [ (self is Greenwich sidereal time) and
            (eLong is a longitude east of Greenwich in radians) ->
              return a new SiderealTime representing the LST
              at longitude eLong ]
        """
        #-- 1 --
        # [ deltaHours  :=  eLong expressed in hours ]
        deltaHours  =  radiansToHours ( eLong )

        #-- 2 --
        gmtHours  =  (self.hours + deltaHours) % 24.0

        #-- 3 --
        return SiderealTime ( gmtHours )
</programlisting>
    </section> <!--SiderealTime-lst-->
    <section id='SiderealTime-fromDatetime'>
      <title><code >SiderealTime.fromDatetime()</code >: Convert
      UTC to GST (static method)</title>
      <para>
        Given a <code >datetime.datetime</code > instance <code
        >dt</code >, this method returns the Greenwich Sidereal
        Time at the UTC time represented by <code >dt</code >.
        In <link linkend='references' >Duffett-Smith</link >,
        this is section 12.  Constant C is defined as
        &#x201c;1.002 738&#x201d;.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   S i d e r e a l T i m e . f r o m D a t e t i m e

    SIDEREAL_C  =  1.002738

#   @staticmethod
    def fromDatetime ( dt ):
        """Convert civil time to Greenwich Sidereal.

          [ dt is a datetime.datetime instance ->
              if  dt has time zone information ->
                return the GST at the UTC equivalent to dt
              else ->
                return the GST assuming dt is UTC ]
        """
</programlisting>
      <para>
        First we have to convert <code >dt</code > to UTC.
        This involves finding out whether <code >dt</code > is
        naive (devoid of time zone information) or aware (fully
        supplied with time zone information).
      </para>
      <para>
        According to the documentation for Python's <ulink
        url='http://docs.python.org/lib/module-datetime.html'
        ><code >datetime</code > module</ulink >, the test for
        awareness has two parts: the instance's <code
        >tzinfo</code > attribute must be not <code >None</code
        >, and <code >dt.tzinfo.utcoffset(dt)</code > must not be
        <code >None</code >.
      </para>
      <para>
        If both these conditions are true, the value returned by
        <code >dt.tzinfo.utcoffset(dt)</code > is the difference
        between UTC and the local time: positive for local times
        east of Greenwich, negative for west.  The value may be
        either a number expressed in minutes, or a <code
        >datetime.timedelta</code > instance, with the shift
        expressed in minutes.  In either case, we can subtract
        the value from <code >dt</code > to get the UTC as a new
        <code >datetime.datetime</code > instance.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ if  dt is naive ->
        #     utc  :=  dt
        #   else ->
        #     utc  :=  the UTC time equivalent to dt ]
        utc  =  dt
        tz  =  dt.tzinfo
        if  tz is not None:
            offset  =  tz.utcoffset ( dt )
            if  offset is not None:
                utc  =  dt - offset
</programlisting>
      <para>
        Step 1 in <link linkend='references' >Duffett-Smith</link
        > says: &#x201c;Find the number of days between January
        0.0 and the date in question.&#x201d; See <xref
        linkend='dayNo' />.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 2 --
        # [ nDays  :=  number of days between January 0.0 and utc ]
        nDays  =  dayNo ( utc )
</programlisting>
      <para>
        In steps 2-3, we compute T<subscript >0</subscript > as
        <code >(nDays&#x00d7;A-B)</code >.  For constant A, see
        <xref linkend='SIDEREAL_A' />.  Factor B is a function of
        the year number, and is computed in <xref
        linkend='SiderealTime-factorB' />.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        t0  =  ( nDays * SIDEREAL_A -
                 SiderealTime.factorB ( utc.year ) )
</programlisting>
      <para>
        Step 4: &#x201c;Convert GMT to decimal hours.&#x201d;
        This conversion uses <xref linkend='dmsUnits' />.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 4 --
        # [ decUTC  :=  utc as decimal hours ]
        floatSec  =  utc.second + float ( utc.microsecond ) / 1e6
        decUTC  =  dmsUnits.mixToSingle (
                       (utc.hour, utc.minute, floatSec) )
</programlisting>
      <para>
        Step 5: &#x201c;Multiply by constant C.&#x201d;, which
        is defined as &#x201c;1.002 738&#x201d;.
      </para>
      <para>
        Step 6: &#x201c;Add this to T<subscript >0</subscript
        >,&#x201d; which, normalized to the interval [0,24), is
        the GST in hours.  The result is returned as a <code
        >SiderealTime</code > instance.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 4 --
        # [ gst  :=  (decUTC * C + t0), normalized to interval [0,24) ]
        gst  =  ( decUTC * SiderealTime.SIDEREAL_C + t0) % 24.0

        #-- 5 --
        return SiderealTime ( gst )
</programlisting>
      <para>
        This is a static method.
      </para>
      <programlisting role='outFile:&spy;'
>    fromDatetime  =  staticmethod ( fromDatetime )
</programlisting>
    </section> <!--SiderealTime-fromDatetime-->
  </section> <!--class-SiderealTime-->
  <section id='class-AltAz'>
    <title><code >class AltAz</code >: Horizon coordinates</title>
    <para>
      An instance of this class represents a location relative to
      where a particular observer is standing at a particular
      time.  <firstterm >Altitude</firstterm > is the distance
      above the horizon, and <firstterm >azimuth</firstterm > is
      the compass direction, 0&#x00b0; for north, 90&#x00b0; for
      east, and so on.
    </para>
    <programlisting role='outFile:&spy;'>
# - - - - -   c l a s s   A l t A z

class AltAz:
    """Represents a sky location in horizon coords. (altitude/azimuth)

      Exports/Invariants:
        .alt:   [ altitude in radians, in [-pi,+pi] ]
        .az:    [ azimuth in radians, in [0,2*pi] ]
    """
</programlisting>
    <section id='AltAz-init'>
      <title><code >AltAz.__init__()</code >: Constructor</title>
      <para>
        The constructor is pro-forma: just save the arguments in
        the instance's namespace.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   A l t A z . _ _ i n i t _ _

    def __init__ ( self, alt, az ):
        """Constructor for AltAz, horizon coordinates.

          [ (alt is an altitude in radians) and
            (az is an azimuth in radians) ->
              return a new AltAz instance with those values,
              normalized as per class invariants ]
        """
        self.alt  =  alt
        self.az  =  az
</programlisting>
    </section> <!--AltAz-init-->
    <section id='AltAz-raDec'>
      <title><code >AltAz.raDec()</code >: Horizon to equatorial
      coordinates</title>
      <para>
        For a given observer's location and time, this method
        returns, as a <code >RADec</code > instance, the
        equatorial coordinates corresponding to <code >self</code
        >'s horizon coordinates.
      </para>
      <para>
        In <link linkend='references' >Duffett-Smith</link >, this is
        section 26.  Here are the relevant formulae:
      </para>
      <informalequation>
        <!--Source is t26-1.tex
         !-->
        <mediaobject>
          <imageobject role="html">
            <imagedata fileref="t26-1.jpg"/>
          </imageobject>
          <imageobject role="fo">
            <imagedata fileref="t26-1.pdf"/>
          </imageobject>
       </mediaobject>
      </informalequation>
      <informaltable>
        <tgroup cols="2">
          <colspec align="left"/>
          <colspec align="left"/>
          <tbody>
            <row>
              <entry valign="top">
                <code><replaceable >a</replaceable ></code>
              </entry>
              <entry valign="top">Altitude.</entry>
            </row>
            <row>
              <entry valign="top">
                <code><replaceable >A</replaceable ></code>
              </entry>
              <entry valign="top">Azimuth.</entry>
            </row>
            <row>
              <entry valign="top">&#x03c6;</entry>
              <entry valign="top">
                The observer's geographic latitude.
              </entry>
            </row>
            <row>
              <entry valign="top">&#x03b4;</entry>
              <entry valign="top">
                Declination.
              </entry>
            </row>
            <row>
              <entry valign="top">
                <code ><replaceable >H</replaceable ></code >
              </entry>
              <entry valign="top">Hour angle.</entry>
            </row>
          </tbody>
        </tgroup>
      </informaltable>
      <programlisting role='outFile:&spy;'
># - - -   A l t A z . r a D e c

    def raDec ( self, lst, latLon ):
        """Convert horizon coordinates to equatorial.

          [ (lst is a local sidereal time as a SiderealTime instance) and
            (latLon is the observer's position as a LatLon instance) ->
              return the corresponding equatorial coordinates as a
              RADec instance ]            
        """
</programlisting>
      <para>
        As <link linkend='references' >Duffett-Smith</link > points
        out, the horizon-to-equatorial conversion in either direction
        uses the same formula.  Hence, the actual calculation of
        &#x03b4; and <code ><replaceable >H</replaceable ></code > is
        handled by <xref linkend='coordRotate' />.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ dec  :=  declination of self at latLon in radians
        #   hourRadians  :=  hour angle of self at latlon in radians ]
        dec, hourRadians  =  coordRotate ( self.alt, latLon.lat,
                                           self.az )

        #-- 2 --
        # [ hourRadians is an hour angle in radians ->
        #     h  :=  hourRadians in hours ]
        h  =  radiansToHours ( hourRadians )
</programlisting>
      <para>
        The conversion of hour angle and LST to right ascension uses
        the same technique as in <xref linkend='hourAngleToRA' />:
        subtract the hour angle from the LST, and normalize.  Then we
        convert it to radians, instantiate an <code >RADec</code >,
        and return the instance.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 3 --
        # [ ra  :=  right ascension for hour angle (h) at local
        #           sidereal time (lst) and location (latLon) ]
        ra  =  hoursToRadians ( ( lst.hours - h ) % 24.0 )

        #-- 4 --
        return  RADec ( ra, dec )
</programlisting>
    </section> <!--AltAz-raDec-->
    <section id='AltAz-str'>
      <title><code >AltAz.__str__()</code >: Convert to a
      string</title>
      <para>
        The return value has this form:
      </para>
      <programlisting
>[az NNNd NN' NN.NNN" alt NNd NN' NN.NNN"]
</programlisting>
      <programlisting role='outFile:&spy;'
># - - -   A l t A z . _ _ s t r _ _

    def __str__ ( self ):
        """Convert self to a string.
        """
</programlisting>
      <para>
        Conversion and formatting as mixed units is handled by <xref
        linkend='dmsUnits' />.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ altList  :=  self.alt, formatted as degrees, minutes,
        #       and seconds
        #   azList  :=  self.az, formatted as degrees, minutes, and
        #       seconds ]
        altList  =  dmsUnits.format ( dmsUnits.singleToMix ( 
            degrees(self.alt) ), lz=True, decimals=3 )
        azList  =  dmsUnits.format ( dmsUnits.singleToMix (
            degrees(self.az) ), lz=True, decimals=3 )

        #-- 2 --
        return ( "[az %sd %s' %s\" alt %sd %s' %s\"]" %
                 (tuple(azList)+tuple(altList)) )
</programlisting>
    </section> <!--AltAz-str-->
  </section> <!--class-AltAz-->
  <section id='coordRotate'>
    <title><code >coordRotate()</code >: Rotation of spherical
    coordinates</title>
    <para>
      This function is used by both <xref linkend='AltAz-raDec' /> and
      <xref linkend='RADec-altAz' />.  Refer to those sections
      for the relevant formulae.
    </para>
    <programlisting role='outFile:&spy;'
># - - -   c o o r d R o t a t e

def coordRotate ( x, y, z ):
    """Used to convert between equatorial and horizon coordinates.

      [ x, y, and z are angles in radians ->
          return (xt, yt) where
          xt=arcsin(sin(x)*sin(y)+cos(x)*cos(y)*cos(z)) and
          yt=arccos((sin(x)-sin(y)*sin(xt))/(cos(y)*cos(xt))) ]
    """
</programlisting>
    <para>
      Step 1: Find sin <code >x<subscript >t</subscript ></code >
      = sin <code ><replaceable >x</replaceable ></code >
      sin<code ><replaceable >y</replaceable ></code > + cos
      <code ><replaceable >x</replaceable ></code > cos<code
      ><replaceable >y</replaceable ></code > cos <code
      ><replaceable >z</replaceable ></code >.  Step 2: Take
      inverse sine to find <code ><replaceable >x<subscript
      >t</subscript ></replaceable ></code >.
    </para>
    <programlisting role='outFile:&spy;'
>    #-- 1 --
    xt  =  asin ( sin(x) * sin(y) +
                  cos(x) * cos(y) * cos(z) )
</programlisting>
    <para>
      Step 3: Find cos <code ><replaceable >y<subscript
      >t</subscript ></replaceable ></code > = (sin <code
      ><replaceable >x</replaceable ></code > - sin <code
      ><replaceable >y</replaceable ></code > sin<code
      ><replaceable >x<subscript >t</subscript ></replaceable
      ></code >) / (cos <code ><replaceable >y</replaceable
      ></code > cos <code ><replaceable >x<subscript
      >t</subscript ></replaceable ></code > ). Step 4: Take
      inverse cos to find <code ><replaceable >y<subscript
      >t</subscript ></replaceable ></code >.
    </para>
    <programlisting role='outFile:&spy;'
>    #-- 2 --
    yt  =  acos ( ( sin(x) - sin(y) * sin(xt) ) /
                  ( cos(y) * cos(xt) ) )
</programlisting>
    <para>
      Next we must remove the ambiguity caused by the computation
      of inverse cosine.  The rule here is that if sin(<code
      ><replaceable >z</replaceable ></code >) is posive, we must
      replace <code ><replaceable >y<subscript >t</subscript
      ></replaceable></code > by 2&#x03c0;-<code ><replaceable
      >y<subscript >t</subscript ></replaceable ></code >.
    </para>
    <programlisting role='outFile:&spy;'
>    #-- 3 --
    if  sin(z) > 0.0:
        yt  =  TWO_PI - yt

    #-- 4 --
    return (xt, yt)
</programlisting>
  </section> <!--coordRotate-->
  <section id='class-RADec'>
    <title><code >class RADec</code >: Equatorial
    coordinates</title>
    <programlisting role='outFile:&spy;'>
# - - - - -   c l a s s   R A D e c

class RADec:
    """Represents a celestial location in equatorial coordinates.

      Exports/Invariants:
        .ra:      [ right ascension in radians ]
        .dec:     [ declination in radians ]
    """
</programlisting>
    <section id='RADec-init'>
      <title><code >RADec.__init__()</code >: Constructor</title>
      <programlisting role='outFile:&spy;'
># - - -   R A D e c . _ _ i n i t _ _

    def __init__ ( self, ra, dec ):
        """Constructor for RADec.
        """
        self.ra  =  ra % TWO_PI
        self.dec  =  dec
</programlisting>
    </section> <!--RADec-init-->
    <section id='RADec-hourAngle'>
      <title><code >RADec.hourAngle()</code >: Find the hour
      angle</title>
      <para>
        This is a convenience function that returns the hour angle for
        a specific time <code >utc</code > and longitude <code
        >eLong</code >.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   R A D e c . h o u r A n g l e

    def hourAngle ( self, utc, eLong ):
        """Find the hour angle at a given observer's location.

          [ (utc is a Universal Time as a datetime.datetime) and
            (eLong is an east longitude in radians) ->
              return the hour angle of self at that time and
              longitude, in radians ]
        """
</programlisting>
      <para>
        The work is passed along to the standalone function
        <xref linkend='raToHourAngle' />.
      </para>
      <programlisting role='outFile:&spy;'
>        return  raToHourAngle ( self.ra, utc, eLong )
</programlisting>
    </section> <!--RADec-hourAngle-->
    <section id='RADec-altAz'>
      <title><code >RADec.altAz()</code >: Equatorial to horizon
      coordinates</title>
      <para>
        In <link linkend='references' >Duffett-Smith</link >, this is
        section 25.
      </para>
      <para>
        Here are the equations defining the transformation:
      </para>
      <informalequation>
        <!--Source is t25-1.tex
         !-->
        <mediaobject>
          <imageobject role="html">
            <imagedata fileref="t25-1.jpg"/>
          </imageobject>
          <imageobject role="fo">
            <imagedata fileref="t25-1.pdf"/>
          </imageobject>
        </mediaobject>
      </informalequation>
      <para>
        For the definitions of the symbols, see <xref
        linkend='AltAz-raDec' />.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   R A D e c . a l t A z

    def altAz ( self, h, lat ):
        """Convert equatorial to horizon coordinates.

          [ (h is an object's hour angle in radians) and
            (lat is the observer's latitude in radians) ->
              return self's position in the observer's sky
              in horizon coordinates as an AltAz instance ]
        """
</programlisting>
      <para>
        As in <xref linkend='AltAz-raDec' />, we will delegate most of
        the work to <xref linkend='coordRotate' />.
      </para>
      <programlisting role='outFile:&spy;'
>        #-- 1 --
        # [ alt  :=  altitude of self as seen from latLon at utc
        #   az  :=  azimuth of self as seen from latLon at utc ]
        alt, az  =  coordRotate ( self.dec, lat, h )

        #-- 2 --
        return AltAz ( alt, az )
</programlisting>
    </section> <!--RADec-altAz-->
    <section id='RADec-str'>
      <title><code >RADec.__str__()</code >: Convert to a
      string</title>
      <para>
        This method displays the coordinates in user-friendly units.
      </para>
      <programlisting role='outFile:&spy;'
># - - -   R A D e c . _ _ s t r _ _

    def __str__ ( self ):
        """Return self as a string.
        """
        #-- 1 --
        # [ raUnits  :=  units of self.ra as hours/minutes/seconds
        #   decUnits  :=  units of self.dec as degrees/minutes/seconds
        raUnits  =  dmsUnits.format (
            dmsUnits.singleToMix ( radiansToHours(self.ra) ),
            lz=True, decimals=3 )
        decUnits  =  dmsUnits.format (
            dmsUnits.singleToMix ( degrees(self.dec) ),
            lz=True, decimals=3 )

        #-- 2 --
        return ( "[%sh %sm %ss, %sd %s' %s\"]" %
                 (tuple(raUnits)+tuple(decUnits)) )
</programlisting>
    </section> <!--RADec-str-->
  </section> <!--class-RADec-->
  <section id='tests'>
    <title>Regression tests</title>
    <para>
      This section contains a number of small scripts that exercise
      the &py; module.  Their names are keyed to the section numbers
      in <link linkend='references' >Duffett-Smith</link >, except
      for tests of modules that are not from the book.
    </para>
    <section id='testmix'>
      <title><code >testmix</code >: Test <code >MixedUnits</code
      ></title>
      <para>
        This script exercises the <code >MixedUnits</code > class by
        using it to convert various mixed values in the
        days-hours-minutes-seconds system, convert them back, and
        format them with and without left zero fill.
      </para>
      <programlisting role='outFile:testmix'
>#!/usr/bin/env python
#================================================================
# testmix: Exercise the MixedUnits class.
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------

from sidereal import *

dhmsSystem = MixedUnits ( (24, 60, 60) )

testValues = [
    (),
    (3.9,),
    (2,12),
    (1,4,10),
    (0,1,30,15),
    (1, 2, 3, 4, 5),   # Not valid
    (1, 0, 0, 59.999999),
    (1, 0, 0, 59.9),
    (1, 2, 3, 59.5),
    (1, 0, 0, 59.499999) ]
              
def test(seq):
    """Test one tuple.
    """
    #--
    # Convert the sequence to a single value.  This can fail
    # if the sequence is too long.
    #--
    try:              
        single = dhmsSystem.mixToSingle(seq)
    except ValueError, detail:
        print "Bad value:", detail
        return

    #--
    # Convert it back to a sequence, and show all three values.
    # Then format it in a range of precisions, with/without zeroes.
    #--
    check = dhmsSystem.singleToMix(single)
    print "%s -> %s -> %s" % (seq, single, check)
    for  digits in range(4):
        noZeroes = dhmsSystem.format ( check, digits )
        withZeroes = dhmsSystem.format ( check, digits, lz=True )
        print "    %d %s %s" % (digits, noZeroes, withZeroes)

#================================================================
# Main
#----------------------------------------------------------------

for  valueList in testValues:
    test(valueList)
</programlisting>
    </section> <!--testmix-->
    <section id='test4-5'>
      <title><code >test4-5</code >: Converting to and from
      Julian day</title>
      <para>
        This script tests <code >JulianDate.fromDatetime()</code
        > and <code >JulianDate.datetime()</code >, algorithms 4
        and 5 respectively in <link linkend='references'
        >Duffett-Smith</link >.
      </para>
      <programlisting role='outFile:test4-5'
>#!/usr/bin/env python
#================================================================
# test4-5: Test datetime &lt;-&gt; JulianDate conversion.
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------

import datetime
from sidereal import *

#--
# First make a datetime.datetime from 1985-02-17T06:00.
#--

dt = datetime.datetime ( 1985, 2, 17, 6 )
print "Input datetime:", dt

#--
# Convert to Julian Date and display.
#--

jd = JulianDate.fromDatetime(dt)
print float(jd)

#--
# Convert back to a datetime object and display that.
#--

check = jd.datetime()
print "Check datetime:", check
</programlisting>
    </section> <!--test4-5-->
    <section id='test12-13'>
      <title><code >test12-13</code >: GMT to GST and back</title>
      <para>
        The example for section 12 in <link linkend='references'
        >Duffett-Smith</link >, conversion of GMT to GST, is:
        What was the GST at 14h 36m 51.67s on April 22nd 1980?
        Their answer is 04h, 40m, 5.17s.
      </para>
      <programlisting role='outFile:test12-13'
>#!/usr/bin/env python
#================================================================
# test12-13: Test GMT &lt;-&gt; GST conversion.
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------

import datetime
import sidereal

#--
# Create a datetime for 1980-04-22T14:36:51.67.
#--
dt  =  datetime.datetime ( 1980, 4, 22, 14, 36, 51, 670000 )
print "Start date:", dt

#--
# Convert dt to GST.
#--
gst  =  sidereal.SiderealTime.fromDatetime ( dt )
print "GST:", gst

#--
# Convert GST back to a datetime.
#--
check  =  gst.utc(dt)
print "Check date:", check
</programlisting>
    </section> <!--test12-13-->
    <section id='test14-15'>
      <title><code >test14-15</code >: Converting between GST and
      LST</title>
      <para>
        Here's the book's example for GST to LST; the second half
        is just the same conversion reversed.
      </para>
      <blockquote>
        <para>
          What is the local sidereal time on the longitude
          64&#x00b0; W when the Greenwich sidereal time is 4h 40m
          5.17s?
        </para>
      </blockquote>
      <para>
        The book's answer is 0h 24m 5.17s.
      </para>
      <programlisting role='outFile:test14-15'
>#!/usr/bin/env python
#================================================================
# test14-15: Test GST &lt;-&gt; LST conversion.
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------

import math
import datetime
import sidereal

#--
# First we used MixedUnits to convert 4h 40m 5.17s to decimal hours.
#--
dmsUnits  =  sidereal.MixedUnits ( (60, 60) )
gstHours  =  dmsUnits.mixToSingle ( (4, 40, 5.17) )
gst  =  sidereal.SiderealTime ( gstHours )
print "GST is", gst

#--
# Convert 64 degrees W to radians of E. Long.
#--
eLong = math.radians ( -64.0 )
print "Longitude east of Greenwich is", math.degrees(eLong)

#--
# Convert GST to LST.
#--
lst  =  gst.lst ( eLong )
print "LST is", lst

#--
# Convert LST back to GST.
#--
check  =  lst.gst ( eLong )
print "Check GST is", check
</programlisting>
    </section> <!--test14-15-->
    <section id='test24'>
      <title><code >test24</code >: Converting between right
      ascension and hour angle</title>
      <para>
        This test has two parts; the second part is the reverse
        conversion from the first part.
      </para>
      <blockquote>
        <para>
          Find the local hour-angle of a star whose right
          ascension is &#x03b1;=18h 32m 21s, at a point whose
          longitude is 64&#x00b0; W, on April 22nd 1980 at 14h
          36m 51.67s GMT.  Answer: 5h 51m 44s.
        </para>
      </blockquote>
      <programlisting role='outFile:test24'
>#!/usr/bin/env python
#================================================================
# test24: Convert from right ascension to hour angle and back.
# For documentation, see:
#   &selfURL;
#----------------------------------------------------------------

import sys
from math import *
import datetime
import sidereal
</programlisting>
      <para>
        First we convert the value of &#x03b1; to radians.
        For the conversion of mixed units to degrees, see <xref
        linkend='MixedUnits-mixToSingle' />; <code
        >radians()</code > is from Python's <code >math</code >
        module and converts degrees to radians.
        Next we convert 64&#x00b0; to radians.  Then we make a <code
        >datetime.datetime</code > instance for the given time.
      </para>
      <programlisting role='outFile:test24'
># - - -   m a i n

def main():
    """Main program.
    """
    global dmsUnits

    #-- 1 --
    # [ dmsUnits  :=  a MixedUnits instance for factor list (60,60) ]
    dmsUnits  =  sidereal.MixedUnits ( (60, 60) )

    #-- 2 --
    # [ ra  :=  (18h 32m 21s) in radians
    #   eLong  :=  64 degrees as radians
    #   dt  :=  14h 36m 51.67s GMT as a datetime.datetime ]
    raHours  =  dmsUnits.mixToSingle((18,32,21))
    ra  =  sidereal.hoursToRadians(raHours)
    eLong  =  radians(-64.0)
    dt  =  datetime.datetime ( 1980, 4, 22, 14, 36, 51, 670000 )
    print "Right ascension:", radHMS(ra)
    print "Longitude:", radDMS(eLong)
    print "UTC:", dt
</programlisting>
    <para>
      The function <xref linkend='hourAngleToRA' /> returns the hour
      angle in radians; we'll format it in degrees, minutes, and
      second for display.
    </para>
    <programlisting role='outFile:test24'
>    #-- 3 --
    # [ hRadians  :=  hour angle in radians for right ascension (ra),
    #       time (dt), and longitude (eLong) ]
    hRadians  =  sidereal.raToHourAngle ( ra, dt, eLong )

    #-- 4 --
    # [ sys.stdout  +:=  hRadians displayed as deg/min/sec ]
    print "Hour angle:", radHMS(hRadians)
</programlisting>
    <para>
      Now, the inverse test.  
    </para>
    <programlisting role='outFile:test24'
>    #-- 5 --
    # [ check  :=  right ascension for hour angle (hRadians),
    #       time (dt), and longitude (eLong) ]
    checkRadians  =  sidereal.hourAngleToRA ( hRadians, dt, eLong )

    #-- 6 --
    # [ sys.stdout  +:=  checkRadians displayed as d/m/s ]
    raDec  =  radHMS ( checkRadians )
    print "Check RA:", radHMS ( checkRadians )
</programlisting>
    <para>
      This subroutine is used to display radians as degrees,
      minutes, and seconds.
    </para>
    <programlisting role='outFile:test24'
># - - -   r a d D M S

def radDMS(rad):
    """Display radians as degrees, minutes, and seconds.
    """
    valueList  =  dmsUnits.format (
        dmsUnits.singleToMix(degrees(rad)), lz=True, decimals=3 )
    return "%sd %s' %s\"" % tuple(valueList)
</programlisting>
    <para>
      This subroutine displays radians as hours, minutes,
      and seconds.
    </para>
    <programlisting role='outFile:test24'
># - - -   r a d H M S

def radHMS(rad):
    """Display radians as hours, minutes, and seconds.
    """
    valueList  =  dmsUnits.format (
        dmsUnits.singleToMix ( sidereal.radiansToHours(rad) ),
                               lz=True, decimals=3 )
    return "%sh %sm %ss" % tuple(valueList)


#================================================================
# Epilogue
#----------------------------------------------------------------

if  __name__ == "__main__":
    main()
</programlisting>
    </section> <!--test24-->
    <section id='test25-26'>
      <title><code >test25-26</code >: Equatorial to horizon
      coordinates and back</title>
      <para>
        This script exercises <xref linkend='AltAz-raDec' /> and <xref
        linkend='RADec-altAz' />, sections 25 and 26 of <link
        linkend='references' >Duffett-Smith</link >.  Here is the
        problem statement for the forward conversion; the book's
        answer is <code ><replaceable >a</replaceable ></code
        >=19&#x00b0; 20&#x2032; 02&#x2033;, <code ><replaceable
        >A</replaceable ></code >=283&#x00b0; 16&#x2032; 18&#x2033;.
      </para>
      <blockquote>
        <para>
          What are the altitude and azimuth of a star whose hour-angle
          is 05h 51m 44s and declination is 23&#x00b0; 13&#x2032;
          10&#x2033;?  The observer's latitude is 52&#x00b0; N.
        </para>
      </blockquote>
      <programlisting role='outFile:test25-26'
>#!/usr/bin/env python
#================================================================
# test25-26:  Test equatorial &lt;-&gt; horizon coordinates.
#   For documentation, see:
#     &selfURL;
#----------------------------------------------------------------

import math
import datetime
import sidereal
</programlisting>
      <para>
        First we set up a <code >MixedUnits</code > instance to do
        mixed-unit conversion and formatting.  Then we set up the
        inputs: the equatorial coordinates, the hour angle, and
        the observer's latitude.
      </para>
      <programlisting role='outFile:test25-26'
>#--
# Converter for mixed units
#--
dmsUnits = sidereal.MixedUnits ( (60, 60) )

#--
# Set up inputs
#--

dec  =  math.radians ( dmsUnits.mixToSingle ( (23, 13, 10) ) )
raDec = sidereal.RADec ( 0.0, dec )
hHours  =  dmsUnits.mixToSingle ( (5, 51, 44) )
hRadians  =  sidereal.hoursToRadians ( hHours )
lat = math.radians ( 52.0 )
latLon = sidereal.LatLon ( lat, 0.0 )
</programlisting>
      <para>
        The result returned by <code >RADec.altAz</code > is an <code
        >AltAz</code > instance, which we then display.
      </para>
      <programlisting role='outFile:test25-26'
>#--
# Convert to horizon coordinates
#--
altAz  =  raDec.altAz ( hRadians, lat )
print "Horizon coordinates:", altAz
</programlisting>
      <para>
        Finally we do the back-conversion.
      </para>
      <programlisting role='outFile:test25-26'
>#--
# Convert back to an hour angle.
#--
gst = sidereal.SiderealTime ( dmsUnits.mixToSingle ( (0, 24, 05) ) )
check = altAz.raDec ( gst, latLon )
print "Check RA/Dec:", check
</programlisting>
    </section> <!--test25-26-->
  </section> <!--tests-->
</article>

