For #406, add Time constructors from datetimes

This was previously an implicit power of `ts.utc()` which was more
difficult for users to find.
This commit is contained in:
Brandon Rhodes 2020-07-17 09:41:48 -04:00
parent d23ce4e1ee
commit 24b0ac3700
5 changed files with 83 additions and 55 deletions

View File

@ -71,6 +71,8 @@ any given version of Skyfield will fall gradually out of date.
.. autosummary::
Timescale.now
Timescale.from_datetime
Timescale.from_datetimes
Timescale.utc
Timescale.tai
Timescale.tai_jd

View File

@ -42,8 +42,8 @@ or how early I need to rise to see the morning sky:
next_midnight = midnight + dt.timedelta(days=1)
ts = load.timescale(builtin=True)
t0 = ts.utc(midnight)
t1 = ts.utc(next_midnight)
t0 = ts.from_datetime(midnight)
t1 = ts.from_datetime(next_midnight)
eph = load('de421.bsp')
bluffton = Topos('40.8939 N', '83.8917 W')
f = almanac.dark_twilight_day(eph, bluffton)

View File

@ -235,7 +235,7 @@ and pass the result to Skyfield:
d = datetime(2014, 1, 16, 1, 32, 9)
e = eastern.localize(d)
t = ts.utc(e)
t = ts.from_datetime(e)
And if Skyfield returns a Julian date at the end of a calculation,
you can ask the Julian date object to build a ``datetime`` object

View File

@ -68,29 +68,36 @@ def test_timescale_utc_method_with_array_inside(ts):
def test_that_building_time_from_naive_datetime_raises_exception(ts):
with assert_raises(ValueError) as info:
ts.utc(datetime(1973, 12, 29, 23, 59, 48))
ts.from_datetime(datetime(1973, 12, 29, 23, 59, 48))
assert 'import timezone' in str(info.exception)
def test_building_time_from_single_utc_datetime(ts):
t = ts.from_datetime(datetime(1973, 12, 29, 23, 59, 48, tzinfo=utc))
assert t.tai == 2442046.5
t = ts.utc(datetime(1973, 12, 29, 23, 59, 48, tzinfo=utc))
assert t.tai == 2442046.5
def test_building_time_from_single_utc_datetime_with_timezone(ts):
tz = timezone('US/Eastern')
t = ts.utc(tz.localize(datetime(2020, 5, 10, 12, 44, 13, 797865)))
t = ts.from_datetime(tz.localize(datetime(2020, 5, 10, 12, 44, 13, 797865)))
dt, leap_second = t.utc_datetime_and_leap_second()
assert dt == datetime(2020, 5, 10, 16, 44, 13, 797865, tzinfo=utc)
assert leap_second == 0
def test_building_time_from_list_of_utc_datetimes(ts):
t = ts.utc([
datetimes = [
datetime(1973, 12, 29, 23, 59, 48, tzinfo=utc),
datetime(1973, 12, 30, 23, 59, 48, tzinfo=utc),
datetime(1973, 12, 31, 23, 59, 48, tzinfo=utc),
datetime(1974, 1, 1, 23, 59, 47, tzinfo=utc),
datetime(1974, 1, 2, 23, 59, 47, tzinfo=utc),
datetime(1974, 1, 3, 23, 59, 47, tzinfo=utc),
])
]
t = ts.from_datetimes(datetimes)
assert list(t.tai) == [
2442046.5, 2442047.5, 2442048.5, 2442049.5, 2442050.5, 2442051.5,
]
t = ts.utc(datetimes)
assert list(t.tai) == [
2442046.5, 2442047.5, 2442048.5, 2442049.5, 2442050.5, 2442051.5,
]
@ -162,7 +169,7 @@ def test_utc_datetime_and_leap_second(ts):
def test_utc_datetime_microseconds_round_trip(ts):
dt = datetime(2020, 5, 10, 11, 50, 9, 727799, tzinfo=utc)
t = ts.utc(dt)
t = ts.from_datetime(dt)
dt2, leap_second = t.utc_datetime_and_leap_second()
assert dt2 == dt
assert leap_second == 0

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import datetime as dt
import re
from collections import namedtuple
from datetime import date, datetime
@ -32,21 +33,18 @@ class CalendarArray(ndarray):
@property
def second(self): return self[5]
try:
from datetime import timezone
utc = timezone.utc
except ImportError:
if hasattr(dt, 'timezone'):
utc = dt.timezone.utc
else:
try:
from pytz import utc
except ImportError:
# Lacking a full suite of timezones from pytz, we at least need a
# time zone object for UTC.
from datetime import timedelta, tzinfo
class UTC(tzinfo):
class UTC(dt.tzinfo):
'UTC'
zero = timedelta(0)
zero = dt.timedelta(0)
def utcoffset(self, dt):
return self.zero
def tzname(self, dt):
@ -58,6 +56,7 @@ except ImportError:
# Much of the following code is adapted from the USNO's "novas.c".
_time_zero = dt.time()
_half_minute = 30.0 / DAY_S
_half_second = 0.5 / DAY_S
_half_microsecond = 0.5e-6 / DAY_S
@ -106,47 +105,62 @@ class Timescale(object):
correct UTC date and time.
"""
return self.utc(self._utcnow().replace(tzinfo=utc))
return self.from_datetime(self._utcnow().replace(tzinfo=utc))
def from_datetime(self, datetime):
"""Return a `Time` for a Python ``datetime``.
The ``datetime`` must be timezone-aware: it must have a time
zone object as its ``tzinfo`` attribute instead of ``None``.
"""
jd, fr = _utc_datetime_to_tai(
self.leap_dates, self.leap_offsets, datetime)
t = Time(self, jd, fr + tt_minus_tai)
t.tai_fraction = fr
return t
def from_datetimes(self, datetime_list):
"""Return a `Time` for a Python ``datetime`` list.
The ``datetime`` objects must each be timezone-aware: they
must each have a time zone object as their ``tzinfo`` attribute
instead of ``None``.
"""
leap_dates = self.leap_dates
leap_offsets = self.leap_offsets
pairs = [_utc_datetime_to_tai(leap_dates, leap_offsets, d)
for d in datetime_list]
jd, fr = zip(*pairs)
t = Time(self, _to_array(jd), fr + tt_minus_tai)
t.tai_fraction = fr
return t
def utc(self, year, month=1, day=1, hour=0, minute=0, second=0.0):
"""Build a `Time` from a UTC calendar date.
You can either specify the date as separate components, or
provide a time zone aware Python datetime. The following two
calls are equivalent (the ``utc`` time zone object can be
imported from the ``skyfield.api`` module, or from ``pytz`` if
you have it)::
ts.utc(2014, 1, 18, 1, 35, 37.5)
ts.utc(datetime(2014, 1, 18, 1, 35, 37, 500000, tzinfo=utc))
Note that only by passing the components separately can you
specify a leap second, because a Python datetime will not allow
the value 60 in its seconds field.
Specify the date as a numeric year, month, day, hour, minute,
and second. Any argument may be an array in which case the
return value is a ``Time`` representing a whole array of times.
"""
# TODO: someday deprecate passing datetime objects here, as
# there are now separate constructors for them.
if isinstance(year, datetime):
dt = year
tai1, tai2 = _utc_datetime_to_tai(self.leap_dates,
self.leap_offsets, dt)
elif isinstance(year, date):
d = year
tai1, tai2 = _utc_date_to_tai(self.leap_dates, self.leap_offsets, d)
elif hasattr(year, '__len__') and isinstance(year[0], datetime):
# TODO: clean this up and better document the possibilities.
list_of_datetimes = year
tai1, tai2 = array([
_utc_datetime_to_tai(self.leap_dates, self.leap_offsets, dt)
for dt in list_of_datetimes
]).T
else:
tai1, tai2 = _utc_to_tai(
self.leap_dates, self.leap_offsets, _to_array(year),
_to_array(month), _to_array(day), _to_array(hour),
_to_array(minute), _to_array(second),
)
return self.from_datetime(year)
if isinstance(year, date):
return self.from_datetime(dt.combine(year, _time_zero))
if hasattr(year, '__len__') and isinstance(year[0], datetime):
return self.from_datetimes(year)
tai1, tai2 = _utc_to_tai(
self.leap_dates, self.leap_offsets, _to_array(year),
_to_array(month), _to_array(day), _to_array(hour),
_to_array(minute), _to_array(second),
)
t = Time(self, tai1, tai2 + tt_minus_tai)
t.tai = tai1 + tai2
t.tai_fraction = tai2
return t
def tai(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
@ -326,7 +340,7 @@ class Time(object):
# TODO: raise non-IndexError exception if this Time is not an array;
# otherwise, a `for` loop over it will not raise an error.
t = Time(self.ts, self.whole[index], self.tt_fraction[index])
for name in 'tai', 'tdb_fraction', 'ut1_fraction':
for name in 'tai_fraction', 'tdb_fraction', 'ut1_fraction':
value = getattr(self, name, None)
if value is not None:
if getattr(value, 'shape', None):
@ -653,15 +667,15 @@ class Time(object):
"""Decimal Julian years centered on J2000.0 = TT 2000 January 1 12h."""
return (self.whole - 1721045.0 + self.tt_fraction) / 365.25
@reify
def tai(self):
return self.tt - tt_minus_tai
@reify
def utc(self):
utc = self._utc_tuple()
return array(utc).view(CalendarArray) if self.shape else CalendarTuple(*utc)
@reify
def tai_fraction(self):
return self.tt_fraction - tt_minus_tai
@reify
def tdb_fraction(self):
fr = self.tt_fraction
@ -701,6 +715,10 @@ class Time(object):
# Low-precision floats generated from internal float pairs.
@property
def tai(self):
return self.whole + self.tai_fraction
@property
def tt(self):
return self.whole + self.tt_fraction
@ -899,7 +917,8 @@ _format_uses_minutes = re.compile(r'%[-_0^#EO]*[MR]').search
def _utc_datetime_to_tai(leap_dates, leap_offsets, dt):
if dt.tzinfo is None:
raise ValueError(_naive_complaint)
dt = dt.astimezone(utc)
if dt.tzinfo is not utc:
dt = dt.astimezone(utc)
return _utc_to_tai(leap_dates, leap_offsets,
dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second + dt.microsecond * 1e-6)