diff --git a/skyfield/tests/test_timelib.py b/skyfield/tests/test_timelib.py index 4e536a0e..b61470c9 100644 --- a/skyfield/tests/test_timelib.py +++ b/skyfield/tests/test_timelib.py @@ -189,6 +189,9 @@ def test_strftime_with_microseconds(): assert t.tai_strftime('%Y %S %f') == '1980 27 816000' assert t.tt_strftime('%Y %S %f') == '1980 00 000000' assert t.tdb_strftime('%Y %S %f') == '1980 59 998471' + assert t.utc_strftime('%f/%f %%f %%%f') == ( + '816000/816000 %f %816000' + ) t = ts.tt(1980, 9, [12, 12]) assert t.utc_strftime('%Y %S %f') == ['1980 08 816000'] * 2 @@ -196,6 +199,9 @@ def test_strftime_with_microseconds(): assert t.tai_strftime('%Y %S %f') == ['1980 27 816000'] * 2 assert t.tt_strftime('%Y %S %f') == ['1980 00 000000'] * 2 assert t.tdb_strftime('%Y %S %f') == ['1980 59 998471'] * 2 + assert t.utc_strftime('%f/%f %%f %%%f') == [ + '816000/816000 %f %816000', + ] * 2 def test_tai_fraction_loses_no_precision(ts): t = ts.tai_jd(2459008.0, 0.0123456789) diff --git a/skyfield/timelib.py b/skyfield/timelib.py index a82ffadd..5b2c4c5a 100644 --- a/skyfield/timelib.py +++ b/skyfield/timelib.py @@ -9,7 +9,7 @@ nan, ndarray, nonzero, pi, rollaxis, searchsorted, sin, where, zeros_like, ) -from time import strftime, struct_time +from time import strftime from ._compatibility import interp from .constants import ASEC2RAD, B1950, DAY_S, T0, tau from .curvelib import Splines, build_spline_given_ends @@ -216,10 +216,7 @@ def _strftime(self, format, jd, fraction, seconds_bump=None): # Python forces an unhappy choice upon us: either use the faster # time.strftime() and lose support for '%f', or use the slower # datetime.strftime() and crash if years are negative. We take the - # first option, but then patch '%f' support back in by secretly - # passing the microseconds string as the time zone name. After all, - # the routines supported by this function never use time zones. - # What could go wrong? + # first option, then render microseconds separately. offset, ms = _strftime_offset_seconds(format) fraction = fraction + offset / DAY_S @@ -238,19 +235,17 @@ def _strftime(self, format, jd, fraction, seconds_bump=None): yday = z if ms: - format = format[:ms.start()] + '%Z' + format[ms.end():] second = (second * 1e6).astype(int) second, usec = divmod(second, 1000000) if seconds_bump is not None: second += seconds_bump if getattr(jd, 'ndim', 0): - u = ['%06d' % u for u in usec] tup = (year, month, day, hour, minute, second, - weekday, yday, z, u) - return [strftime(format, struct_time(t)) for t in zip(*tup)] - u = '%06d' % usec - tup = year, month, day, hour, minute, second, weekday, yday, z, u - return strftime(format, struct_time(tup)) + weekday, yday, z) + return [_strftime_with_microseconds(format, ms, t, u) + for t, u in zip(zip(*tup), usec)] + tup = year, month, day, hour, minute, second, weekday, yday, z + return _strftime_with_microseconds(format, ms, tup, usec) else: second = second.astype(int) if seconds_bump is not None: @@ -1193,11 +1188,42 @@ def build_delta_t_table(delta_t_recent): [[end_tt], [delta_t_parabola_morrison_stephenson_2004(end_J)]], ) -_format_uses_milliseconds = re.compile(r'%[-_0^#EO]*f').search +_microsecond_directive = re.compile(r'%[-_0^#EO]*f').match _format_uses_seconds = re.compile(r'%[-_0^#EO]*[STXc]').search _format_uses_minutes = re.compile(r'%[-_0^#EO]*[MR]').search _format_uses_day_of_year = re.compile(r'%[-_0^#EO]*j').search +def _find_microsecond_directives(format): + matches = [] + i = 0 + while True: + i = format.find('%', i) + if i < 0: + return matches + if format.startswith('%%', i): + i += 2 + continue + match = _microsecond_directive(format, i) + if match is None: + i += 1 + else: + matches.append(match) + i = match.end() + +def _strftime_with_microseconds(format, matches, tup, usec): + pieces = [] + end = 0 + for match in matches: + chunk = format[end:match.start()] + if chunk: + pieces.append(strftime(chunk, tup)) + pieces.append('%06d' % usec) + end = match.end() + chunk = format[end:] + if chunk: + pieces.append(strftime(chunk, tup)) + return ''.join(pieces) + def _datetime_to_utc_tuple(dt): z = dt.tzinfo if z is None: @@ -1216,7 +1242,7 @@ def _normalize_jd_and_fraction(jd, fraction): return jd, fraction def _strftime_offset_seconds(format): - uses_ms = _format_uses_milliseconds(format) + uses_ms = _find_microsecond_directives(format) if uses_ms: if _OLD_PYTHON: raise ValueError('strftime() "%f" not supported under Python 2') @@ -1237,17 +1263,15 @@ def _strftime(format, year, month, day, hour, minute, second, #_format_uses_day_of_year(format) if uses_ms: - format = format[:uses_ms.start()] + '%Z' + format[uses_ms.end():] second = (second * 1e6).astype(int) second, usec = divmod(second, 1000000) if getattr(year, 'ndim', 0): - u = ['%06d' % u for u in usec] tup = (year, month, day, hour, minute, second, - weekday, yday, zero, u) - return [strftime(format, struct_time(t)) for t in zip(*tup)] - u = '%06d' % usec - tup = year, month, day, hour, minute, second, weekday, yday, zero, u - return strftime(format, struct_time(tup)) + weekday, yday, zero) + return [_strftime_with_microseconds(format, uses_ms, t, u) + for t, u in zip(zip(*tup), usec)] + tup = year, month, day, hour, minute, second, weekday, yday, zero + return _strftime_with_microseconds(format, uses_ms, tup, usec) else: second = second.astype(int) tup = year, month, day, hour, minute, second, weekday, yday, zero