33"""
44
55import logging
6+ from argparse import ArgumentTypeError
7+ import numpy as np
68
79logger = logging .getLogger (__name__ )
810
@@ -11,16 +13,22 @@ def _check_lon_type_180(lon_in):
1113 """
1214 Checks value range of longitude with type 180
1315 """
14- if not - 180 <= lon_in <= 180 :
15- raise ValueError (f"lon_in needs to be in the range [-180, 180]: { lon_in } " )
16+ lon_min = np .min (lon_in )
17+ lon_max = np .max (lon_in )
18+ for lon in [lon_min , lon_max ]:
19+ if not - 180 <= lon <= 180 :
20+ raise ValueError ("(All values of) lon_in must be in the range [-180, 180]" )
1621
1722
1823def _check_lon_type_360 (lon_in ):
1924 """
2025 Checks value range of longitude with type 360
2126 """
22- if not 0 <= lon_in <= 360 :
23- raise ValueError (f"lon_in needs to be in the range [0, 360]: { lon_in } " )
27+ lon_min = np .min (lon_in )
28+ lon_max = np .max (lon_in )
29+ for lon in [lon_min , lon_max ]:
30+ if not 0 <= lon <= 360 :
31+ raise ValueError ("(All values of) lon_in must be in the range [0, 360]" )
2432
2533
2634def _check_lon_value_given_type (lon_in , lon_type_in ):
@@ -50,6 +58,31 @@ def _convert_lon_type_180_to_360(lon_in):
5058 return lon_out
5159
5260
61+ def _detect_lon_type (lon_in ):
62+ """
63+ Detect longitude type of a given numeric. If lon_in contains more than one number (as in a list
64+ or Numpy array), this function will assume all members are of the same type if (a) there is at
65+ least one unambiguous member and (b) all unambiguous members are of the same type.
66+ """
67+ lon_min = np .min (lon_in )
68+ lon_max = np .max (lon_in )
69+ if lon_min < - 180 :
70+ raise ValueError (f"(Minimum) longitude < -180: { lon_min } " )
71+ if lon_max > 360 :
72+ raise ValueError (f"(Maximum) longitude > 360: { lon_max } " )
73+ min_type_180 = lon_min < 0
74+ max_type_360 = lon_max > 180
75+ if min_type_180 and max_type_360 :
76+ raise RuntimeError ("Longitude array contains values of both types 180 and 360" )
77+ if not min_type_180 and not max_type_360 :
78+ raise ArgumentTypeError ("Longitude(s) ambiguous; could be type 180 or 360" )
79+ if min_type_180 :
80+ lon_type = 180
81+ else :
82+ lon_type = 360
83+ return lon_type
84+
85+
5386def _convert_lon_type_360_to_180 (lon_in ):
5487 """
5588 Convert a longitude from type 360 to type 180
@@ -70,6 +103,22 @@ def _convert_lon_type_360_to_180(lon_in):
70103 return lon_out
71104
72105
106+ def check_other_is_lontype (other ):
107+ """
108+ Used in comparison operators to throw an error if the "other" object being compared isn't also
109+ a Longitude object. This makes it so that comparing longitudes requires that both sides of the
110+ comparison must be Longitude objects and thus must have a specified longitude type (180 or 360).
111+
112+ We could try to coerce non-Longitude `other` to Longitude, but that might result in
113+ situations where tests think everything works but code will fail if `other` is
114+ ambiguous.
115+ """
116+ if not isinstance (other , Longitude ):
117+ raise TypeError (
118+ f"Comparison not supported between instances of 'Longitude' and '{ type (other )} '"
119+ )
120+
121+
73122class Longitude :
74123 """
75124 A class to keep track of a longitude and its type
@@ -93,6 +142,41 @@ def __init__(self, lon, lon_type):
93142 self ._lon = lon
94143 self ._lon_type = lon_type
95144
145+ def _check_lons_same_type (self , other ):
146+ """
147+ If you're comparing two Longitudes of different types in different hemispheres, then
148+ `lon1 > lon2` and `lon2 < lon1` will incorrectly give different answers! We could make it so
149+ that this doesn't fail as long as symmetricality isn't violated, but that might lead to
150+ unexpected failures in practice.
151+ """
152+ if self .lon_type () != other .lon_type ():
153+ raise TypeError ("Comparison not supported between Longitudes of different types" )
154+
155+ # __eq__ makes it so that == and != both work.
156+ def __eq__ (self , other ):
157+ check_other_is_lontype (other )
158+ return self ._lon == other .get (self ._lon_type )
159+
160+ def __lt__ (self , other ):
161+ check_other_is_lontype (other )
162+ self ._check_lons_same_type (other )
163+ return self ._lon < other ._lon
164+
165+ def __gt__ (self , other ):
166+ check_other_is_lontype (other )
167+ self ._check_lons_same_type (other )
168+ return self ._lon > other ._lon
169+
170+ def __le__ (self , other ):
171+ check_other_is_lontype (other )
172+ self ._check_lons_same_type (other )
173+ return self ._lon <= other ._lon
174+
175+ def __ge__ (self , other ):
176+ check_other_is_lontype (other )
177+ self ._check_lons_same_type (other )
178+ return self ._lon >= other ._lon
179+
96180 def get (self , lon_type_out ):
97181 """
98182 Get the longitude value, converting longitude type if needed
@@ -104,3 +188,9 @@ def get(self, lon_type_out):
104188 if lon_type_out == 360 :
105189 return _convert_lon_type_180_to_360 (self ._lon )
106190 raise RuntimeError (f"Add handling for lon_type_out { lon_type_out } " )
191+
192+ def lon_type (self ):
193+ """
194+ Getter method for self._lon_type
195+ """
196+ return self ._lon_type
0 commit comments