From 703f924210462076438f53fb91a97c0566cbd3a2 Mon Sep 17 00:00:00 2001 From: Don Cross Date: Fri, 3 Jan 2020 14:03:01 -0500 Subject: [PATCH] Python: Added Time.Parse function. Added function Time.Parse to convert a UTC date/time string into a Time object. People should not have to keep reinventing that wheel. I will be able to simplify astro_demo_common.py. --- generate/template/astronomy.py | 53 ++++++++++++++++++++++++++++++++++ generate/test.py | 28 ++++++++++++++++++ source/python/README.md | 25 ++++++++++++++++ source/python/astronomy.py | 53 ++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+) diff --git a/generate/template/astronomy.py b/generate/template/astronomy.py index 78609d5e..0d6f958e 100644 --- a/generate/template/astronomy.py +++ b/generate/template/astronomy.py @@ -34,6 +34,7 @@ https://github.com/cosinekitty/astronomy import math import datetime import enum +import re _PI2 = 2.0 * math.pi _EPOCH = datetime.datetime(2000, 1, 1, 12) @@ -179,6 +180,11 @@ class Error(Exception): def __init__(self, message): Exception.__init__(self, message) +class DateTimeFormatError(Error): + """The syntax of a UTC date/time string was not valid, or it contains invalid values.""" + def __init__(self, text): + Error.__init__(self, 'The date/time string is not valid: "{}"'.format(text)) + class EarthNotAllowedError(Error): """The Earth is not allowed as the celestial body in this calculation.""" def __init__(self): @@ -271,6 +277,8 @@ def _DeltaT(mjd): def _TerrestrialTime(ut): return ut + _DeltaT(ut + _Y2000_IN_MJD) / 86400.0 +_TimeRegex = re.compile(r'^([0-9]{1,4})-([0-9]{2})-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2}(\.[0-9]+)?))?Z)?$') + class Time: """Represents a date and time used for performing astronomy calculations. @@ -320,6 +328,51 @@ class Time: self.tt = _TerrestrialTime(ut) self.etilt = None + @staticmethod + def Parse(text): + """Creates a #Time object from a string of the form 'yyyy-mm-ddThh:mm:ss.sssZ' + + Parses a UTC date and time from a string and returns a #Time object. + Permits a subset of ISO 8601 format. + The year, month, and day are required. + Hours, minutes, seconds, and fractions of a second are optional. + If time is specified, there must be a 'T' between the date and the time + and a 'Z' at the end of the time. + + Parameters + ---------- + text : string + A string of the following formats: + `yyyy-mm-dd` + `yyyy-mm-ddThh:mmZ` + `yyyy-mm-ddThh:mm:ssZ` + `yyyy-mm-ddThh:mm:ss.sssZ` + + Returns + ------- + Time + """ + m = _TimeRegex.match(text) + if m is None: + raise DateTimeFormatError(text) + year = int(m.group(1)) + month = int(m.group(2)) + if not (1 <= month <= 12): + raise DateTimeFormatError(text) + day = int(m.group(3)) + if not (1 <= day <= 31): + raise DateTimeFormatError(text) + hour = int(m.group(5) or '0') + if not (0 <= hour <= 23): + raise DateTimeFormatError(text) + minute = int(m.group(6) or '0') + if not (0 <= minute <= 59): + raise DateTimeFormatError(text) + second = float(m.group(8) or '0') + if not (0.0 <= second < 60.0): + raise DateTimeFormatError(text) + return Time.Make(year, month, day, hour, minute, second) + @staticmethod def Make(year, month, day, hour, minute, second): """Creates a #Time object from a UTC calendar date and time. diff --git a/generate/test.py b/generate/test.py index 3b05a3ac..df079cf9 100755 --- a/generate/test.py +++ b/generate/test.py @@ -7,6 +7,23 @@ import astronomy #----------------------------------------------------------------------------------------------------------- +def AssertGoodTime(text, correct): + time = astronomy.Time.Parse(text) + check = str(time) + if check != correct: + print('Python AssertGoodTime FAILURE: parsed "{}", got "{}", expected "{}"'.format(text, check, correct)) + sys.exit(1) + print('AssertGoodTime: "{}" OK'.format(text)) + +def AssertBadTime(text): + try: + astronomy.Time.Parse(text) + except astronomy.DateTimeFormatError: + print('AssertBadTime: "{}" OK'.format(text)) + else: + print('Python AssertBadTime FAILURE: should not have parsed "{}"'.format(text)) + sys.exit(1) + def Test_AstroTime(): expected_ut = 6910.270978506945 expected_tt = 6910.271779431480 @@ -34,6 +51,17 @@ def Test_AstroTime(): print('Test_AstroTime: expected 2019-01-01T00:00:00.000Z but found {}'.format(s)) sys.exit(1) print('Current time =', astronomy.Time.Now()) + AssertGoodTime('2015-12-31', '2015-12-31T00:00:00.000Z') + AssertGoodTime('2015-12-31T23:45Z', '2015-12-31T23:45:00.000Z') + AssertGoodTime('2015-01-02T23:45:17Z', '2015-01-02T23:45:17.000Z') + AssertGoodTime('1971-03-17T03:30:55.976Z', '1971-03-17T03:30:55.976Z') + AssertBadTime('') + AssertBadTime('1971-13-01') + AssertBadTime('1971-12-32') + AssertBadTime('1971-12-31T24:00:00Z') + AssertBadTime('1971-12-31T23:60:00Z') + AssertBadTime('1971-12-31T23:00:60Z') + AssertBadTime('1971-03-17T03:30:55.976') #----------------------------------------------------------------------------------------------------------- diff --git a/source/python/README.md b/source/python/README.md index 71a43699..87d0a2a7 100644 --- a/source/python/README.md +++ b/source/python/README.md @@ -400,6 +400,24 @@ calculate current observational conditions. ### Returns: [`Time`](#Time) + +### Time.Parse(text) + +**Creates a [`Time`](#Time) object from a string of the form 'yyyy-mm-ddThh:mm:ss.sssZ'** + +Parses a UTC date and time from a string and returns a [`Time`](#Time) object. +Permits a subset of ISO 8601 format. +The year, month, and day are required. +Hours, minutes, seconds, and fractions of a second are optional. +If time is specified, there must be a 'T' between the date and the time +and a 'Z' at the end of the time. + +| Type | Parameter | Description | +| --- | --- | --- | +| `string` | `text` | A string of the following formats: `yyyy-mm-dd` `yyyy-mm-ddThh:mmZ` `yyyy-mm-ddThh:mm:ssZ` `yyyy-mm-ddThh:mm:ss.sssZ` | + +### Returns: [`Time`](#Time) + ### Time.Utc(self) @@ -539,6 +557,13 @@ A vector magnitude is too small to have a direction in space. --- + +### DateTimeFormatError + +The syntax of a UTC date/time string was not valid, or it contains invalid values. + +--- + ### EarthNotAllowedError diff --git a/source/python/astronomy.py b/source/python/astronomy.py index 2348f7d3..a6fa1376 100644 --- a/source/python/astronomy.py +++ b/source/python/astronomy.py @@ -34,6 +34,7 @@ https://github.com/cosinekitty/astronomy import math import datetime import enum +import re _PI2 = 2.0 * math.pi _EPOCH = datetime.datetime(2000, 1, 1, 12) @@ -179,6 +180,11 @@ class Error(Exception): def __init__(self, message): Exception.__init__(self, message) +class DateTimeFormatError(Error): + """The syntax of a UTC date/time string was not valid, or it contains invalid values.""" + def __init__(self, text): + Error.__init__(self, 'The date/time string is not valid: "{}"'.format(text)) + class EarthNotAllowedError(Error): """The Earth is not allowed as the celestial body in this calculation.""" def __init__(self): @@ -362,6 +368,8 @@ def _DeltaT(mjd): def _TerrestrialTime(ut): return ut + _DeltaT(ut + _Y2000_IN_MJD) / 86400.0 +_TimeRegex = re.compile(r'^([0-9]{1,4})-([0-9]{2})-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2}(\.[0-9]+)?))?Z)?$') + class Time: """Represents a date and time used for performing astronomy calculations. @@ -411,6 +419,51 @@ class Time: self.tt = _TerrestrialTime(ut) self.etilt = None + @staticmethod + def Parse(text): + """Creates a #Time object from a string of the form 'yyyy-mm-ddThh:mm:ss.sssZ' + + Parses a UTC date and time from a string and returns a #Time object. + Permits a subset of ISO 8601 format. + The year, month, and day are required. + Hours, minutes, seconds, and fractions of a second are optional. + If time is specified, there must be a 'T' between the date and the time + and a 'Z' at the end of the time. + + Parameters + ---------- + text : string + A string of the following formats: + `yyyy-mm-dd` + `yyyy-mm-ddThh:mmZ` + `yyyy-mm-ddThh:mm:ssZ` + `yyyy-mm-ddThh:mm:ss.sssZ` + + Returns + ------- + Time + """ + m = _TimeRegex.match(text) + if m is None: + raise DateTimeFormatError(text) + year = int(m.group(1)) + month = int(m.group(2)) + if not (1 <= month <= 12): + raise DateTimeFormatError(text) + day = int(m.group(3)) + if not (1 <= day <= 31): + raise DateTimeFormatError(text) + hour = int(m.group(5) or '0') + if not (0 <= hour <= 23): + raise DateTimeFormatError(text) + minute = int(m.group(6) or '0') + if not (0 <= minute <= 59): + raise DateTimeFormatError(text) + second = float(m.group(8) or '0') + if not (0.0 <= second < 60.0): + raise DateTimeFormatError(text) + return Time.Make(year, month, day, hour, minute, second) + @staticmethod def Make(year, month, day, hour, minute, second): """Creates a #Time object from a UTC calendar date and time.