diff --git a/bindings/python/test/test_converters.py b/bindings/python/test/test_converters.py index 6be10ae02..527c0cb03 100644 --- a/bindings/python/test/test_converters.py +++ b/bindings/python/test/test_converters.py @@ -1,5 +1,7 @@ # Apache License 2.0 +import pytest + from datetime import datetime, timedelta, timezone import numpy as np @@ -17,6 +19,7 @@ from ostk.astrodynamics.converters import coerce_to_datetime from ostk.astrodynamics.converters import coerce_to_instant +from ostk.astrodynamics.converters import coerce_to_iso from ostk.astrodynamics.converters import coerce_to_interval from ostk.astrodynamics.converters import coerce_to_duration from ostk.astrodynamics.converters import coerce_to_position @@ -25,25 +28,231 @@ def test_coerce_to_datetime_success_instant(): - value = Instant.date_time(DateTime(2020, 1, 1), Scale.UTC) - assert coerce_to_datetime(value) == datetime(2020, 1, 1, tzinfo=timezone.utc) + value = Instant.date_time(DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.UTC) + assert coerce_to_datetime(value) == datetime( + 2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc + ) + + value = Instant.date_time(DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.TAI) + assert coerce_to_datetime(value) == datetime( + 2020, 1, 2, 3, 3, 28, 123456, tzinfo=timezone.utc + ) def test_coerce_to_datetime_success_datetime(): - value = datetime(2020, 1, 1, tzinfo=timezone.utc) + value = datetime(2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc) + assert coerce_to_datetime(value) == value + + value = datetime( + 2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone(timedelta(seconds=3600)) + ) assert coerce_to_datetime(value) == value +def test_coerce_to_datetime_success_iso(): + value = "2020-01-02T03:04:05.123456+00:00" + assert coerce_to_datetime(value) == datetime( + 2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc + ) + + value = "2020-01-02T03:04:05+00:00" + assert coerce_to_datetime(value) == datetime(2020, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + + value = "2020-01-02T03:04:05.123456+01:00" + assert coerce_to_datetime(value) == datetime( + 2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone(timedelta(seconds=3600)) + ) + + value = "2020-01-02T03:04:05.123456Z" + assert coerce_to_datetime(value) == datetime( + 2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc + ) + + value = "2020-01-02T03:04:05Z" + assert coerce_to_datetime(value) == datetime(2020, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + + +def test_coerce_to_datetime_failure(): + with pytest.raises(TypeError): + coerce_to_datetime(False) + + with pytest.raises(Exception): + coerce_to_datetime("some_ill_formed_iso") + + def test_coerce_to_instant_success_datetime(): - value = datetime(2020, 1, 1, tzinfo=timezone.utc) - assert coerce_to_instant(value) == Instant.date_time(DateTime(2020, 1, 1), Scale.UTC) + value = datetime(2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc) + assert coerce_to_instant(value) == Instant.date_time( + DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.UTC + ) + + value = datetime( + 2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone(timedelta(seconds=3600)) + ) + assert coerce_to_instant(value) == Instant.date_time( + DateTime(2020, 1, 2, 2, 4, 5, 123, 456), Scale.UTC + ) def test_coerce_to_instant_success_instant(): - value = Instant.date_time(DateTime(2020, 1, 1), Scale.UTC) + value = Instant.date_time(DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.UTC) + assert coerce_to_instant(value) == value + + value = Instant.date_time(DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.TAI) assert coerce_to_instant(value) == value +def test_coerce_to_instant_success_iso(): + value = "2020-01-02T03:04:05.123456+00:00" + assert coerce_to_instant(value) == Instant.date_time( + DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.UTC + ) + + value = "2020-01-02T03:04:05+00:00" + assert coerce_to_instant(value) == Instant.date_time( + DateTime(2020, 1, 2, 3, 4, 5), Scale.UTC + ) + + value = "2020-01-02T03:04:05.123456+01:00" + assert coerce_to_instant(value) == Instant.date_time( + DateTime(2020, 1, 2, 2, 4, 5, 123, 456), Scale.UTC + ) + + value = "2020-01-02T03:04:05.123456Z" + assert coerce_to_instant(value) == Instant.date_time( + DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.UTC + ) + + value = "2020-01-02T03:04:05Z" + assert coerce_to_instant(value) == Instant.date_time( + DateTime( + 2020, + 1, + 2, + 3, + 4, + 5, + ), + Scale.UTC, + ) + + +def test_coerce_to_instant_failure(): + with pytest.raises(TypeError): + coerce_to_instant(False) + + with pytest.raises(Exception): + coerce_to_instant("some_ill_formed_iso") + + +def test_coerce_to_iso_success_datetime(): + value = datetime(2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc) + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.123456+00:00" + ) + + value = datetime( + 2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone(timedelta(seconds=3600)) + ) + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.123456+01:00" + ) + + value = datetime(2020, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc) + assert ( + coerce_to_iso(value, timespec="milliseconds") == "2020-01-02T03:04:05.123+00:00" + ) + + value = datetime(2020, 1, 2, 3, 4, 5, 0, tzinfo=timezone.utc) + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.000000+00:00" + ) + + +def test_coerce_to_iso_success_instant(): + value = Instant.date_time(DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.UTC) + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.123456+00:00" + ) + + value = Instant.date_time(DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.TAI) + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:03:28.123456+00:00" + ) + + value = Instant.date_time(DateTime(2020, 1, 2, 3, 4, 5, 123, 456), Scale.UTC) + assert ( + coerce_to_iso(value, timespec="milliseconds") == "2020-01-02T03:04:05.123+00:00" + ) + + value = Instant.date_time(DateTime(2020, 1, 2, 3, 4, 5, 123), Scale.UTC) + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.123000+00:00" + ) + + +def test_coerce_to_iso_success_iso(): + value = "2020-01-02T03:04:05.123456+00:00" + assert coerce_to_iso(value, timespec="microseconds") == value + + value = "2020-01-02T03:04:05.123456+01:00" + assert coerce_to_iso(value, timespec="microseconds") == value + + value = "2020-01-02T03:04:05.123456+00:00" + assert ( + coerce_to_iso(value, timespec="milliseconds") == "2020-01-02T03:04:05.123+00:00" + ) + + value = "2020-01-02T03:04:05.123+00:00" + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.123000+00:00" + ) + + value = "2020-01-02T03:04:05+00:00" + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.000000+00:00" + ) + + value = "2020-01-02T03:04:05.123456Z" + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.123456+00:00" + ) + + value = "2020-01-02T03:04:05.123456Z" + assert ( + coerce_to_iso(value, timespec="milliseconds") == "2020-01-02T03:04:05.123+00:00" + ) + + value = "2020-01-02T03:04:05.123Z" + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.123000+00:00" + ) + + value = "2020-01-02T03:04:05Z" + assert ( + coerce_to_iso(value, timespec="microseconds") + == "2020-01-02T03:04:05.000000+00:00" + ) + + +def test_coerce_to_iso_failure(): + with pytest.raises(TypeError): + coerce_to_iso(False) + + with pytest.raises(Exception): + coerce_to_iso("some_ill_formed_iso") + + def test_coerce_to_interval_success_interval(): value = Interval.closed( Instant.date_time(DateTime(2020, 1, 1), Scale.UTC), diff --git a/bindings/python/tools/python/ostk/astrodynamics/converters.py b/bindings/python/tools/python/ostk/astrodynamics/converters.py index 5618f2d12..9ebb5a712 100644 --- a/bindings/python/tools/python/ostk/astrodynamics/converters.py +++ b/bindings/python/tools/python/ostk/astrodynamics/converters.py @@ -15,12 +15,12 @@ from ostk.physics.coordinate import Frame -def coerce_to_datetime(value: Instant | datetime) -> datetime: +def coerce_to_datetime(value: Instant | datetime | str) -> datetime: """ Return datetime from value. Args: - value (Instant | datetime): A value to coerce. + value (Instant | datetime | str): A value to coerce. Returns: datetime: The coerced datetime. @@ -29,15 +29,21 @@ def coerce_to_datetime(value: Instant | datetime) -> datetime: if isinstance(value, datetime): return value - return value.get_date_time(Scale.UTC).replace(tzinfo=timezone.utc) + if isinstance(value, Instant): + return value.get_date_time(Scale.UTC).replace(tzinfo=timezone.utc) + + if isinstance(value, str): + return datetime.fromisoformat(value) + + raise TypeError("Argument must be a datetime, an Instant, or a str.") -def coerce_to_instant(value: Instant | datetime) -> Instant: +def coerce_to_instant(value: Instant | datetime | str) -> Instant: """ Return Instant from value. Args: - value (Instant | datetime): A value to coerce. + value (Instant | datetime | str): A value to coerce. Returns: Instant: The coerced Instant. @@ -46,7 +52,39 @@ def coerce_to_instant(value: Instant | datetime) -> Instant: if isinstance(value, Instant): return value - return Instant.date_time(value, Scale.UTC) + if isinstance(value, datetime): + return Instant.date_time(value.astimezone(tz=timezone.utc), Scale.UTC) + + if isinstance(value, str): + return coerce_to_instant(coerce_to_datetime(value)) + + raise TypeError("Argument must be a datetime, an Instant, or a str.") + + +def coerce_to_iso( + value: Instant | datetime | str, timespec: str = "microseconds" +) -> Instant: + """ + Return an ISO string from value. + + Args: + value (Instant | datetime | str): A value to coerce. + timespec (str): A time resolution. Defaults to "microseconds". + + Returns: + str: The coerced ISO string. + """ + + if isinstance(value, str): + return coerce_to_iso(coerce_to_datetime(value), timespec=timespec) + + if isinstance(value, datetime): + return value.isoformat(timespec=timespec) + + if isinstance(value, Instant): + return coerce_to_iso(coerce_to_datetime(value), timespec=timespec) + + raise TypeError("Argument must be a datetime, an Instant, or a str.") def coerce_to_interval(