From eec8dc2008dbab8411ee9c90012c88cca1373d05 Mon Sep 17 00:00:00 2001 From: dh1tw Date: Sun, 21 Sep 2014 19:45:43 +0200 Subject: [PATCH] Added module for Locator based calculations (Distance, Heading, Locator -> Lat / Long....etc) --- pyhamtools/locator.py | 354 ++++++++++++++++++++++++ setup.py | 3 +- test/test_dxcluster.py | 69 +++++ test/test_locator_distances.py | 60 ++++ test/test_locator_latlong_to_locator.py | 33 +++ test/test_locator_locator_to_latlong.py | 58 ++++ test/test_locator_sunrise_sunset.py | 57 ++++ 7 files changed, 633 insertions(+), 1 deletion(-) create mode 100644 pyhamtools/locator.py create mode 100644 test/test_dxcluster.py create mode 100644 test/test_locator_distances.py create mode 100644 test/test_locator_latlong_to_locator.py create mode 100644 test/test_locator_locator_to_latlong.py create mode 100644 test/test_locator_sunrise_sunset.py diff --git a/pyhamtools/locator.py b/pyhamtools/locator.py new file mode 100644 index 0000000..7208cc7 --- /dev/null +++ b/pyhamtools/locator.py @@ -0,0 +1,354 @@ +from __future__ import division +from math import pi, sin, cos, atan2, sqrt, radians, log, tan, degrees +from datetime import datetime + +import pytz +import ephem + +UTC = pytz.UTC + +def latlong_to_locator (latitude, longitude): + """converts WGS84 coordinates into the corresponding Maidenhead Locator + + Args: + latitude (float): Latitude + longitude (float): Longitude + + Returns: + string: Maidenhead locator + + Raises: + ValueError: When called with wrong or invalid input args + TypeError: When args are non float values + + Example: + The following example converts latitude and longitude into the Maidenhead locator + + >>> from pyhamtools.locator import latlong_to_locator + >>> latitude = 48.5208333 + >>> longitude = 9.375 + 'JN48QM' + + Note: + Latitude (negative = West, positive = East) + Longitude (negative = South, positive = North) + + """ + + if longitude >= 180 or longitude <= -180: + raise ValueError + + if latitude >= 90 or latitude <= -90: + raise ValueError + + longitude += 180; + latitude +=90; + + locator = chr(ord('A') + int(longitude / 20)) + locator += chr(ord('A') + int(latitude / 10)) + locator += chr(ord('0') + int((longitude % 20) / 2)) + locator += chr(ord('0') + int(latitude % 10)) + locator += chr(ord('A') + int((longitude - int(longitude / 2) * 2) / (2 / 24))) + locator += chr(ord('A') + int((latitude - int(latitude / 1) * 1 ) / (1 / 24))) + + return locator + +def locator_to_latlong (locator): + """converts Maidenhead locator in the corresponding WGS84 coordinates + + Args: + locator (string): Locator, either 4 or 6 characters + + Returns: + tuple (float, float): Latitude, Longitude + + Raises: + ValueError: When called with wrong or invalid input arg + TypeError: When arg is not a string + + Example: + The following example converts a Maidenhead locator into Latitude and Longitude + + >>> from pyhamtools.locator import locator_to_latlong + >>> latitude, longitude = locator_to_latlong("JN48QM") + >>> print latitude, longitude + 48.5208333333 9.375 + + Note: + Latitude (negative = West, positive = East) + Longitude (negative = South, positive = North) + + """ + + locator = locator.upper() + + if len(locator) == 5 or len(locator) < 4: + raise ValueError + + if ord(locator[0]) > ord('R') or ord(locator[0]) < ord('A'): + raise ValueError + + if ord(locator[1]) > ord('R') or ord(locator[1]) < ord('A'): + raise ValueError + + if ord(locator[2]) > ord('9') or ord(locator[2]) < ord('0'): + raise ValueError + + if ord(locator[3]) > ord('9') or ord(locator[3]) < ord('0'): + raise ValueError + + if len(locator) == 6: + if ord(locator[4]) > ord('X') or ord(locator[4]) < ord('A'): + raise ValueError + if ord (locator[5]) > ord('X') or ord(locator[5]) < ord('A'): + raise ValueError + + longitude = (ord(locator[0]) - ord('A')) * 20 - 180 + latitude = (ord(locator[1]) - ord('A')) * 10 - 90 + longitude += (ord(locator[2]) - ord('0')) * 2 + latitude += (ord(locator[3]) - ord('0')) + + if len(locator) == 6: + longitude += ((ord(locator[4])) - ord('A')) * (2 / 24) + latitude += ((ord(locator[5])) - ord('A')) * (1 / 24) + + # move to center of subsquare + longitude += 1 / 24 + latitude += 0.5 / 24 + + else: + # move to center of square + longitude += 1; + latitude += 0.5; + + return latitude, longitude + + +def calculate_distance(locator1, locator2): + """calculates the (shortpath) distance between two Maidenhead locators + + Args: + locator1 (string): Locator, either 4 or 6 characters + locator2 (string): Locator, either 4 or 6 characters + + Returns: + float: Distance in km + + Raises: + ValueError: When called with wrong or invalid input arg + AttributeError: When args are not a string + + Example: + The following calculates the distance between two Maidenhead locators in km + + >>> from pyhamtools.locator import calculate_distance + >>> calculate_distance("JN48QM", "QF67bf") + 16466.413 + + """ + + R = 6371 #earh radius + lat1, long1 = locator_to_latlong(locator1) + lat2, long2 = locator_to_latlong(locator2) + + d_lat = radians(lat2) - radians(lat1) + d_long = radians(long2) - radians(long1) + + r_lat1 = radians(lat1) + r_long1 = radians(long1) + r_lat2 = radians(lat2) + r_long2 = radians(long2) + + a = sin(d_lat/2) * sin(d_lat/2) + cos(r_lat1) * cos(r_lat2) * sin(d_long/2) * sin(d_long/2) + c = 2 * atan2(sqrt(a), sqrt(1-a)) + d = R * c #distance in km + + return d; + + +def calculate_distance_longpath(locator1, locator2): + """calculates the (longpath) distance between two Maidenhead locators + + Args: + locator1 (string): Locator, either 4 or 6 characters + locator2 (string): Locator, either 4 or 6 characters + + Returns: + float: Distance in km + + Raises: + ValueError: When called with wrong or invalid input arg + AttributeError: When args are not a string + + Example: + The following calculates the longpath distance between two Maidenhead locators in km + + >>> from pyhamtools.locator import calculate_distance_longpath + >>> calculate_distance_longpath("JN48QM", "QF67bf") + 23541.5867 + + """ + + c = 40008 #[km] earth circumference + sp = calculate_distance(locator1, locator2) + + return c - sp + + +def calculate_heading(locator1, locator2): + """calculates the heading from the first to the second locator + + Args: + locator1 (string): Locator, either 4 or 6 characters + locator2 (string): Locator, either 4 or 6 characters + + Returns: + float: Heading in deg + + Raises: + ValueError: When called with wrong or invalid input arg + AttributeError: When args are not a string + + Example: + The following calculates the heading from locator1 to locator2 + + >>> from pyhamtools.locator import calculate_heading + >>> calculate_heading("JN48QM", "QF67bf") + 74.3136 + + """ + + lat1, long1 = locator_to_latlong(locator1) + lat2, long2 = locator_to_latlong(locator2) + + r_lat1 = radians(lat1) + r_lon1 = radians(long1) + + r_lat2 = radians(lat2) + r_lon2 = radians(long2) + + d_lon = radians(long2 - long1) + + b = atan2(sin(d_lon)*cos(r_lat2),cos(r_lat1)*sin(r_lat2)-sin(r_lat1)*cos(r_lat2)*cos(d_lon)) # bearing calc + bd = degrees(b) + br,bn = divmod(bd+360,360) # the bearing remainder and final bearing + + return bn + +def calculate_heading_longpath(locator1, locator2): + """calculates the heading from the first to the second locator (long path) + + Args: + locator1 (string): Locator, either 4 or 6 characters + locator2 (string): Locator, either 4 or 6 characters + + Returns: + float: Long path heading in deg + + Raises: + ValueError: When called with wrong or invalid input arg + AttributeError: When args are not a string + + Example: + The following calculates the long path heading from locator1 to locator2 + + >>> from pyhamtools.locator import calculate_heading_longpath + >>> calculate_heading_longpath("JN48QM", "QF67bf") + 254.3136 + + """ + + heading = calculate_heading(locator1, locator2) + + lp = (heading + 180)%360 + + return lp + +def calculate_sunrise_sunset(locator, calc_date=datetime.utcnow()): + """calculates the next sunset and sunrise for a Maidenhead locator at a give date & time + + Args: + locator1 (string): Maidenhead Locator, either 4 or 6 characters + calc_date (datetime, optional): Starting datetime for the calculations (UTC) + + Returns: + dict: Containing datetimes for morning_dawn, sunrise, evening_dawn, sunset + + Raises: + ValueError: When called with wrong or invalid input arg + AttributeError: When args are not a string + + Example: + The following calculates the next sunrise & sunset for JN48QM on the 1./Jan/2014 + + >>> from pyhamtools.locator import calculate_sunrise_sunset + >>> from datetime import datetime + >>> import pytz + >>> UTC = pytz.UTC + >>> myDate = datetime(year=2014, month=1, day=1, tzinfo=UTC) + >>> calculate_sunrise_sunset("JN48QM", myDate) + 74.3136 + + """ + morning_dawn = None + sunrise = None + evening_dawn = None + sunset = None + + latitude, longitude = locator_to_latlong(locator) + + if type(calc_date) != datetime: + raise ValueError + + sun = ephem.Sun() + home = ephem.Observer() + + home.lat = str(latitude) + home.long = str(longitude) + home.date = calc_date + + sun.compute(home) + + try: + nextrise = home.next_rising(sun) + nextset = home.next_setting(sun) + + home.horizon = '-6' + beg_twilight = home.next_rising(sun, use_center=True) + end_twilight = home.next_setting(sun, use_center=True) + + morning_dawn = beg_twilight.datetime() + sunrise = nextrise.datetime() + + evening_dawn = nextset.datetime() + sunset = end_twilight.datetime() + + #if sun never sets or rises (e.g. at polar circles) + except ephem.AlwaysUpError as e: + morning_dawn = None + sunrise = None + evening_dawn = None + sunset = None + except ephem.NeverUpError as e: + morning_dawn = None + sunrise = None + evening_dawn = None + sunset = None + + result = {} + result['morning_dawn'] = morning_dawn + result['sunrise'] = sunrise + result['evening_dawn'] = evening_dawn + result['sunset'] = sunset + + if morning_dawn: + result['morning_dawn'] = morning_dawn.replace(tzinfo=UTC) + if sunrise: + result['sunrise'] = sunrise.replace(tzinfo=UTC) + if evening_dawn: + result['evening_dawn'] = evening_dawn.replace(tzinfo=UTC) + if sunset: + result['sunset'] = sunset.replace(tzinfo=UTC) + print result + return result + \ No newline at end of file diff --git a/setup.py b/setup.py index 459bda1..aa79b09 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ if sys.version_info >= (3,): kw['use_2to3'] = True setup(name='pyhamtools', - version='0.3.1', + version='0.4.0', description='Collection of Tools for Amateur Radio developers', author='Tobias Wellnitz, DH1TW', author_email='Tobias@dh1tw.de', @@ -18,6 +18,7 @@ setup(name='pyhamtools', install_requires=[ "pytz", "requests", + "pyephem", ], **kw ) diff --git a/test/test_dxcluster.py b/test/test_dxcluster.py new file mode 100644 index 0000000..4092978 --- /dev/null +++ b/test/test_dxcluster.py @@ -0,0 +1,69 @@ +import pytest +from datetime import datetime + + +import pytz + + +from pyhamtools.consts import LookupConventions as const +from pyhamtools.dxcluster import decode_char_spot, decode_pc11_message, decode_pc61_message + +UTC = pytz.UTC + +fix_spot1 = "DX de CT3FW: 21004.8 HC2AO 599 TKS(CW)QSL READ,QRZ.COM 2132Z" +fix_spot1_broken_spotter_call = "DX de $QRM: 21004.8 HC2AO 599 TKS(CW)QSL READ,QRZ.COM 2132Z" + +fix_spot_pc11 = "PC11^14010.0^R155AP^30-Apr-2014^2252Z^CQ CQ^R9CAC^RN6BN^H95^~" +fix_spot_pc61 = "PC61^14030.5^ZF2NUE^30-Apr-2014^2253Z^ ^ND4X^W4NJA^72.51.152.150^H96^~" + +fix_spot2 = "DX de DL6NAA: 10368887.0 DL7VTX/B 55s in JO50VFjo62 never hrd B4 1505Z" +fix_spot3 = "DX de CT3FW: 21004.8 IDIOT 599 TKS(CW)QSL READ,QRZ.COM 2132Z" +fix_spot4 = "DX de OK1TEH: 144000.0 C0NTEST -> www.darkside.cz/qrv.php 1328Z JO70" +fix_spot5 = "DX de DK7UK: 50099.0 EA5/ON4CAU JN48QTIM98 QRP 5W LOOP ANT 1206Z" +fix_spot6 = "DX de UA3ZBK: 14170.0 UR8EW/QRP POWER 2-GU81+SPYDER 1211Z" +fix_spot7 = "DX de 9K2/K2SES 14205.0 DK0HY 0921Z" #missing semicolon +fix_spot8 = "DX de DK1CS:9330368887.0 DL7VTX/B 1505Z" +fix_spot9 = "DX de DH1TW: 23.0 DS1TW 1505Z" +fix_spot10 = "DX de DH1TW 234.0 DS1TW 1505Z" +fix_spot11 = "DX de DH1TW: 234.0 DS1TW 1505Z" +fix_spot12 = "DX de DH1TW: 50105.0 ZD6DYA 1505Z" + +response_spot1 = { + const.SPOTTER: "CT3FW", + const.DX: "HC2AO", + const.BAND: 15, + const.MODE: "CW", + const.COMMENT: "599 TKS(CW)QSL READ,QRZ.COM", + const.TIME: datetime.utcnow().replace( hour=21, minute=32, second=0, microsecond = 0, tzinfo=UTC) +} + + +class TestDXClusterSpots: + + def test_spots(self): + assert decode_char_spot(fix_spot1)[const.SPOTTER] == "CT3FW" + assert decode_char_spot(fix_spot1)[const.DX] == "HC2AO" + assert decode_char_spot(fix_spot1)[const.FREQUENCY] == 21004.8 + assert decode_char_spot(fix_spot1)[const.COMMENT] == "599 TKS(CW)QSL READ,QRZ.COM" + assert isinstance(decode_char_spot(fix_spot1)[const.TIME], datetime) + + with pytest.raises(ValueError): + decode_char_spot(fix_spot1_broken_spotter_call) + + + def test_spots_pc11(self): + assert decode_pc11_message(fix_spot_pc11)[const.SPOTTER] == "R9CAC" + assert decode_pc11_message(fix_spot_pc11)[const.DX] == "R155AP" + assert decode_pc11_message(fix_spot_pc11)[const.FREQUENCY] == 14010.0 + assert decode_pc11_message(fix_spot_pc11)[const.COMMENT] == "CQ CQ" + assert decode_pc11_message(fix_spot_pc11)["node"] == "RN6BN" + assert isinstance(decode_pc11_message(fix_spot_pc11)[const.TIME], datetime) + + def test_spots_pc61(self): + assert decode_pc61_message(fix_spot_pc61)[const.SPOTTER] == "ND4X" + assert decode_pc61_message(fix_spot_pc61)[const.DX] == "ZF2NUE" + assert decode_pc61_message(fix_spot_pc61)[const.FREQUENCY] == 14030.5 + assert decode_pc61_message(fix_spot_pc61)[const.COMMENT] == " " + assert decode_pc61_message(fix_spot_pc61)["node"] == "W4NJA" + assert decode_pc61_message(fix_spot_pc61)["ip"] == "72.51.152.150" + assert isinstance(decode_pc61_message(fix_spot_pc61)[const.TIME], datetime) \ No newline at end of file diff --git a/test/test_locator_distances.py b/test/test_locator_distances.py new file mode 100644 index 0000000..4503e25 --- /dev/null +++ b/test/test_locator_distances.py @@ -0,0 +1,60 @@ +import pytest +from pyhamtools.locator import calculate_distance, calculate_distance_longpath, calculate_heading, calculate_heading_longpath +from pyhamtools.consts import LookupConventions as const + +class Test_calculate_distance(): + + def test_calculate_distance_edge_cases(self): + + assert calculate_distance("JN48QM", "JN48QM") == 0 + assert calculate_distance("JN48", "JN48") == 0 + assert abs(calculate_distance("AA00AA", "rr00xx") - 19009) < 1 + + def test_calculate_distance_normal_case(self): + + assert abs(calculate_distance("JN48QM", "FN44AB") - 5965) < 1 + assert abs(calculate_distance("FN44AB", "JN48QM") - 5965) < 1 + assert abs(calculate_distance("JN48QM", "QF67bf") - 16467) < 1 + + def test_calculate_distance_invalid_inputs(self): + with pytest.raises(AttributeError): + calculate_distance(5, 12) + + with pytest.raises(ValueError): + calculate_distance("XX0XX", "ZZ0Z") + + def test_calculate_distance_longpath_normal_case(self): + + assert abs(calculate_distance_longpath("JN48QM", "FN44AB") - 34042) < 1 + assert abs(calculate_distance_longpath("JN48QM", "QF67bf") - 23541) < 1 + + def test_calculate_distance_longpath_edge_cases(self): + + assert abs(calculate_distance_longpath("JN48QM", "JN48QM") - 40008) < 1 + assert abs(calculate_distance_longpath("JN48QM", "AE15UU") - 20645) < 1 #ZL7 Chatham - almost antipods + + +class Test_calculate_heading(): + + def test_calculate_heading_normal_cases(self): + + assert abs(calculate_heading("JN48QM", "FN44AB") - 298) < 1 + assert abs(calculate_heading("FN44AB", "JN48QM") - 54) < 1 + assert abs(calculate_heading("JN48QM", "QF67bf") - 74) < 1 + assert abs(calculate_heading("QF67BF", "JN48QM") - 310) < 1 + + def test_calculate_heading_edge_cases(self): + + assert abs(calculate_heading("JN48QM", "JN48QM") - 0 ) < 1 + + def test_calculate_heading_longpath(self): + + assert abs(calculate_heading_longpath("JN48QM", "FN44AB") - 118) < 1 + assert abs(calculate_heading_longpath("FN44AB", "JN48QM") - 234) < 1 + assert abs(calculate_heading_longpath("JN48QM", "QF67BF") - 254) < 1 + assert abs(calculate_heading_longpath("QF67BF", "JN48QM") - 130) < 1 + + def test_calculate_heading_longpath_edge_cases(self): + + assert abs(calculate_heading_longpath("JN48QM", "JN48QM") - 180 ) < 1 + \ No newline at end of file diff --git a/test/test_locator_latlong_to_locator.py b/test/test_locator_latlong_to_locator.py new file mode 100644 index 0000000..f49a11e --- /dev/null +++ b/test/test_locator_latlong_to_locator.py @@ -0,0 +1,33 @@ +import pytest +from pyhamtools.locator import latlong_to_locator +from pyhamtools.consts import LookupConventions as const + +class Test_latlong_to_locator(): + + def test_latlong_to_locator_edge_cases(self): + assert latlong_to_locator(-89.97916, -179.95833) == "AA00AA" + assert latlong_to_locator(89.97916, 179.9583) == "RR99XX" + + def test_latlong_to_locator_normal_case(self): + + assert latlong_to_locator(48.52083, 9.3750000) == "JN48QM" + assert latlong_to_locator(48.5, 9.0) == "JN48MM" #center of the square + + def test_latlong_to_locator_invalid_characters(self): + + with pytest.raises(ValueError): + latlong_to_locator("JN48QM", "test") + + with pytest.raises(ValueError): + latlong_to_locator("", "") + + def test_latlong_to_locator_out_of_boundry(self): + + with pytest.raises(ValueError): + latlong_to_locator(-90, -180) + + with pytest.raises(ValueError): + latlong_to_locator(90, 180) + + with pytest.raises(ValueError): + latlong_to_locator(10000, 120000) diff --git a/test/test_locator_locator_to_latlong.py b/test/test_locator_locator_to_latlong.py new file mode 100644 index 0000000..f23937d --- /dev/null +++ b/test/test_locator_locator_to_latlong.py @@ -0,0 +1,58 @@ +import pytest +from pyhamtools.locator import locator_to_latlong +from pyhamtools.consts import LookupConventions as const + +class Test_locator_to_latlong(): + + def test_locator_to_latlong_edge_cases(self): + latitude, longitude = locator_to_latlong("AA00AA") + assert abs(latitude + 89.97916) < 0.00001 + assert abs(longitude +179.95833) < 0.0001 + + latitude, longitude = locator_to_latlong("RR99XX") + assert abs(latitude - 89.97916) < 0.00001 + assert abs(longitude - 179.9583) < 0.0001 + + def test_locator_to_latlong_normal_case(self): + + latitude, longitude = locator_to_latlong("JN48QM") + assert abs(latitude - 48.52083) < 0.00001 + assert abs(longitude - 9.3750000) < 0.0001 + + latitude, longitude = locator_to_latlong("JN48") + assert abs(latitude - 48.5) < 0.001 + assert abs(longitude - 9.000) < 0.001 + + def test_locator_to_latlong_mixed_signs(self): + + latitude, longitude = locator_to_latlong("Jn48qM") + assert abs(latitude - 48.52083) < 0.00001 + assert abs(longitude - 9.3750000) < 0.0001 + + + def test_locator_to_latlong_wrong_amount_of_characters(self): + + with pytest.raises(ValueError): + latitude, longitude = locator_to_latlong("J") + + with pytest.raises(ValueError): + latitude, longitude = locator_to_latlong("JN") + + with pytest.raises(ValueError): + latitude, longitude = locator_to_latlong("JN4") + + with pytest.raises(ValueError): + latitude, longitude = locator_to_latlong("JN8Q") + + def test_locator_to_latlong_invalid_characters(self): + + with pytest.raises(ValueError): + latitude, longitude = locator_to_latlong("21XM99") + + with pytest.raises(ValueError): + latitude, longitude = locator_to_latlong("****") + + def test_locator_to_latlong_out_of_boundry(self): + + with pytest.raises(ValueError): + latitude, longitude = locator_to_latlong("RR99XY") diff --git a/test/test_locator_sunrise_sunset.py b/test/test_locator_sunrise_sunset.py new file mode 100644 index 0000000..ed7fa58 --- /dev/null +++ b/test/test_locator_sunrise_sunset.py @@ -0,0 +1,57 @@ +from datetime import datetime, timedelta + +import pytest +import pytz + +from pyhamtools.locator import calculate_sunrise_sunset + +UTC = pytz.UTC + +class Test_calculate_sunrise_sunset_normal_case(): + + def test_calculate_sunrise_sunset(self): + + time_margin = timedelta(minutes=1) + locator = "JN48QM" + + test_time = datetime(year=2014, month=1, day=1, tzinfo=UTC) + result_JN48QM_1_1_2014_evening_dawn = datetime(2014, 1, 1, 15, 38, tzinfo=UTC) + result_JN48QM_1_1_2014_morning_dawn = datetime(2014, 1, 1, 6, 36, tzinfo=UTC) + result_JN48QM_1_1_2014_sunrise = datetime(2014, 1, 1, 7, 14, tzinfo=UTC) + result_JN48QM_1_1_2014_sunset = datetime(2014, 1, 1, 16, 15, 23, 31016, tzinfo=UTC) + + assert calculate_sunrise_sunset(locator, test_time)['morning_dawn'] - result_JN48QM_1_1_2014_morning_dawn < time_margin + assert calculate_sunrise_sunset(locator, test_time)['evening_dawn'] - result_JN48QM_1_1_2014_evening_dawn < time_margin + assert calculate_sunrise_sunset(locator, test_time)['sunset'] - result_JN48QM_1_1_2014_sunset < time_margin + assert calculate_sunrise_sunset(locator, test_time)['sunrise'] - result_JN48QM_1_1_2014_sunrise < time_margin + + def test_calculate_distance_edge_case(self): + + time_margin = timedelta(minutes=1) + locator = "AA00AA" + # no sunrise or sunset at southpol during arctic summer + + test_time = datetime(year=2014, month=1, day=1, tzinfo=UTC) + result_AA00AA_1_1_2014_evening_dawn = datetime(2014, 1, 1, 15, 38, tzinfo=UTC) + result_AA00AA_1_1_2014_morning_dawn = datetime(2014, 1, 1, 6, 36, tzinfo=UTC) + result_AA00AA_1_1_2014_sunrise = datetime(2014, 1, 1, 7, 14, tzinfo=UTC) + result_AA00AA_1_1_2014_sunset = datetime(2014, 1, 1, 16, 15, 23, 31016, tzinfo=UTC) + + assert calculate_sunrise_sunset(locator, test_time)['morning_dawn'] == None + assert calculate_sunrise_sunset(locator, test_time)['evening_dawn'] == None + assert calculate_sunrise_sunset(locator, test_time)['sunset'] == None + assert calculate_sunrise_sunset(locator, test_time)['sunrise'] == None + + def test_calculate_distance_invalid_inputs(self): + + with pytest.raises(ValueError): + calculate_sunrise_sunset("", "") + + with pytest.raises(ValueError): + calculate_sunrise_sunset("JN48QM", "") + + with pytest.raises(ValueError): + calculate_sunrise_sunset("JN48", 55) + + with pytest.raises(AttributeError): + calculate_sunrise_sunset(33, datetime.now())