def test_validate_datetime_iterable(): """Functional test to ensure the validate_datetime() method runs properly on naive datetime iterables.""" iterable = [ pd.to_datetime("January 1, 2020"), pd.to_datetime("January 13, 2020"), ] assert isinstance(validate_datetime(iterable), pd.DatetimeIndex) assert isinstance(validate_datetime(iterable)[0], pd.Timestamp)
def calculate_day_number( date: Union[datetime, str, Iterable[Union[datetime, str]]] ) -> Union[int, Iterable[int]]: """ Method to calculate the day number of the year given a proper date. :param date: A proper datetime object or a string that can be parsed into a proper datetime object. :returns: An integer corresponding to the day number of `date`. """ # Ensure `date` can be parsed into a datetime object date = validate_datetime(datetime_object=date) # Return the day number corresponding to `date` if isinstance(date, pd.Timestamp): return date.dayofyear elif isinstance(date, pd.Series): return date.dt.dayofyear else: return list(date.dayofyear)
def test_invalid_string(): """Test to ensure validate_datetime() will throw a ValueError with an invalid string input.""" with pytest.raises(ValueError): assert validate_datetime("January 1, blah blah blah")
def test_valid_string(): """Test to ensure validate_datetime() can parse a typical datetime string.""" assert isinstance(validate_datetime("January 1, 2019 12:00 PM"), pd.Timestamp)
def test_validate_datetime(dt): """Functional test to ensure the validate_datetime() method runs properly on naive datetime objects.""" assert isinstance(validate_datetime(dt), pd.Timestamp)
def calculate_solar_noon_in_local_standard_time( local_standard_time: Union[datetime, str, Iterable[Union[datetime, str]]], longitude_degrees: Union[int, float], ) -> Union[datetime, Iterable[datetime]]: """ Method to calculate solar noon given a local standard timestamp (including date and time zone offset from UTC) and a location's longitude (in degrees west). The equation used is from Duffie & Beckman (2006) Equation 1.5.2. :param local_standard_time: A `datetime` object, containing a timezone offset, representing the time that will be converted to solar time. :param longitude_degrees: A numeric value representing a location's angular distance west of the meridian at Greenwich, England. `longitude_degrees` should be between 0 and 360 degrees. :returns: A datetime object representing the local standard time that corresponds to solar noon for the given date in `local_standard_time` and `longitude_degrees`. """ # Type- and range-check `longitude_degrees` validate_numeric_value(longitude_degrees, minimum=0, maximum=360) # Validate `local_standard_time` local_ts = validate_datetime(datetime_object=local_standard_time) if isinstance(local_ts, pd.Series): # Ensure local_ts has time zone information if local_ts.dt.tz is None: raise ValueError( """`local_standard_time` must provide a time zone offset, such as `1/1/2019 12:00 PM -06:00`.""") # Calculate offset from UTC, using timezone offset in `local_ts` utc_offset = (local_ts.dt.tz.utcoffset(local_ts).total_seconds() // 3_600) else: # Ensure local_ts has time zone information if local_ts.tzinfo is None: raise ValueError( """`local_standard_time` must provide a time zone offset, such as `1/1/2019 12:00 PM -06:00`.""") # Calculate offset from UTC, using timezone offset in `local_ts` utc_offset = (local_ts.tzinfo.utcoffset(local_ts).total_seconds() // 3_600) """Determine the standard meridian for the given `longitude_degrees`, which corresponds to 15 degrees per hour offset.""" standard_meridian = 15 * abs(utc_offset) E = calculate_E_min( calculate_B_degrees(calculate_day_number(local_standard_time))) longitude_correction_mins = 4.0 * (standard_meridian - longitude_degrees) # Create a datetime object for noon on the same date as `solar_ts` try: solar_noon = np.array([ datetime( year=x.date().year, month=x.date().month, day=x.date().day, hour=12, minute=0, second=0, tzinfo=x.tzinfo, ) for x in local_ts ]) except TypeError: solar_noon = datetime( year=local_ts.date().year, month=local_ts.date().month, day=local_ts.date().day, hour=12, minute=0, second=0, tzinfo=local_ts.tzinfo, ) if isinstance(solar_noon, np.ndarray): result = np.array([ x - timedelta(minutes=E[i] + longitude_correction_mins) for i, x in enumerate(solar_noon) ]) else: result = solar_noon - timedelta(minutes=E + longitude_correction_mins) return result
def convert_to_solar_time( local_standard_time: Union[datetime, str, Iterable[Union[datetime, str]]], longitude_degrees: Union[int, float], ) -> Union[datetime, Iterable[datetime]]: """ Method to calculate solar time given a local standard timestamp (including date and time zone offset from UTC) and a location's longitude (in degrees west). The equation used is from Duffie & Beckman (2006) Equation 1.5.2. :param local_standard_time: A `datetime` object, containing a timezone offset, representing the time that will be converted to solar time. :param longitude_degrees: A numeric value representing a location's angular distance west of the meridian at Greenwich, England. `longitude_degrees` should be between 0 and 360 degrees. :returns: A datetime object representing the solar time corresponding to `local_standard_time` at the given `longitude_degrees`. """ # Type- and range-check `longitude_degrees` validate_numeric_value(longitude_degrees, minimum=0, maximum=360) # Validate `local_standard_time` local_ts = validate_datetime(datetime_object=local_standard_time) if isinstance(local_ts, pd.Series): # Ensure local_ts has time zone information if local_ts.dt.tz is None: raise ValueError( """`local_standard_time` must provide a time zone offset, such as `1/1/2019 12:00 PM -06:00`.""") # Calculate offset from UTC, using timezone offset in `local_ts` utc_offset = (local_ts.dt.tz.utcoffset(local_ts).total_seconds() // 3_600) else: # Ensure local_ts has time zone information if local_ts.tzinfo is None: raise ValueError( """`local_standard_time` must provide a time zone offset, such as `1/1/2019 12:00 PM -06:00`.""") # Calculate offset from UTC, using timezone offset in `local_ts` utc_offset = (local_ts.tzinfo.utcoffset(local_ts).total_seconds() // 3_600) """Determine the standard meridian for the given `longitude_degrees`, which corresponds to 15 degrees per hour offset.""" standard_meridian = 15 * np.abs(utc_offset) E = calculate_E_min( calculate_B_degrees(calculate_day_number(local_standard_time))) longitude_correction_mins = 4.0 * (standard_meridian - longitude_degrees) try: return local_ts + timedelta(minutes=longitude_correction_mins + E) except TypeError: # When working with iterables return [ x + timedelta(minutes=longitude_correction_mins + E[i]) for i, x in enumerate(local_ts) ]