#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2017, AGB & GC
# Full license can be found in License.md
# ----------------------------------------------------------------------------
"""Routines to convert from different file timekeeping methods to datetime."""
import datetime as dt
import numpy as np
[docs]def get_datetime_fmt_len(datetime_fmt):
"""Get the lenght of a string line needed for a specific datetime format.
Parameters
----------
datetime_fmt : str
Formatting string used to convert between datetime and string object
Returns
-------
str_len : int
Minimum length of a string needed to hold the specified data
Notes
-----
See the datetime documentation for meanings of the datetime directives
"""
# Start by setting the base length. This accounts for any non-datetime
# directives in the string length.
str_len = len(datetime_fmt)
# Each of the directives have character lengths that they fill. Add the
# appropriate number of spaces.
add_len = {'%a': 1, '%A': 10, '%b': 1, '%B': 8, '%Y': 2, '%f': 4, '%z': 3,
'%Z': 1, '%j': 1, '%c': 22, '%x': 8, '%X': 7}
for dt_dir in add_len.keys():
if datetime_fmt.find(dt_dir) >= 0:
str_len += add_len[dt_dir]
return str_len
[docs]def year_soy_to_datetime(yyyy, soy):
"""Converts year and soy to datetime.
Parameters
----------
yyyy : int
4 digit year
soy : float
seconds of year
Returns
-------
dtime : dt.datetime
datetime object
"""
# Calcuate doy, hour, min, seconds of day
ss = soy / 86400.0
ddd = np.floor(ss)
ss = (soy - ddd * 86400.0) / 3600.0
hh = np.floor(ss)
ss = (soy - ddd * 86400.0 - hh * 3600.0) / 60.0
mm = np.floor(ss)
ss = soy - ddd * 86400.0 - hh * 3600.0 - mm * 60.0
# Define format
stime = "{:d}-{:.0f}-{:.0f}-{:.0f}-{:.0f}".format(yyyy, ddd + 1, hh, mm,
ss)
# Convert to datetime
dtime = dt.datetime.strptime(stime, "%Y-%j-%H-%M-%S")
return dtime
[docs]def yyddd_to_date(yyddd):
"""Convert from years since 1900 and day of year to datetime.
Parameters
----------
yyddd : str
String containing years since 1900 and day of year
(e.g. 100126 = 2000-05-5).
Returns
-------
dtime : dt.datetime
Datetime object containing date information
"""
if not isinstance(yyddd, str):
raise ValueError("YYDDD must be a string")
# Remove any decimal data
yyddd = yyddd.split(".")[0]
# Select the year
year = int(yyddd[:-3]) + 1900
# Format the datetime string
dtime = dt.datetime.strptime("{:d} {:s}".format(year, yyddd[-3:]), "%Y %j")
return dtime
[docs]def convert_time(year=None, soy=None, yyddd=None, sod=None, date=None,
tod=None, datetime_fmt="%Y-%m-%d %H:%M:%S"):
"""Convert to datetime from multiple time formats.
Parameters
----------
year : int or NoneType
Year or None if not in year-soy format (default=None)
soy : int or NoneType
Seconds of year or None if not in year-soy format (default=None)
yyddd : str or NoneType
String containing years since 1900 and 3-digit day of year
(default=None)
sod : int, float, or NoneType
Seconds of day or None if the time of day is not in this format
(default=None)
date : str or NoneType
String containing date information or None if not in date-time format
(default=None)
tod : str or NoneType
String containing time of day information or None if not in date-time
format (default=None)
datetime_fmt : str
String with the date-time or date format (default='%Y-%m-%d %H:%M:%S')
Returns
-------
dtime : dt.datetime
Datetime object
"""
try:
if year is not None and soy is not None:
dtime = year_soy_to_datetime(year, soy)
else:
if yyddd is not None:
ddate = yyddd_to_date(yyddd)
date = ddate.strftime("%Y-%m-%d")
# Ensure that the datetime format contains current date format
if datetime_fmt.find("%Y-%m-%d") < 0:
ifmt = datetime_fmt.upper().find("YYDDD")
if ifmt >= 0:
old_fmt = datetime_fmt[ifmt:ifmt + 5]
datetime_fmt = datetime_fmt.replace(old_fmt,
"%Y-%m-%d")
else:
datetime_fmt = "%Y-%m-%d {:s}".format(datetime_fmt)
if tod is None:
str_time = "{:}".format(date)
# Ensure that the datetime format does not contain time
for time_fmt in [" %H:%M:%S", " SOD"]:
time_loc = datetime_fmt.upper().find(time_fmt)
if time_loc > 0:
datetime_fmt = datetime_fmt[:time_loc]
else:
str_time = "{:s} {:s}".format(date, tod)
dtime = dt.datetime.strptime(str_time, datetime_fmt)
if sod is not None:
# Add the seconds of day to dtime
microsec, sec = np.modf(sod)
dtime += dt.timedelta(seconds=int(sec))
if microsec > 0.0:
# Add the microseconds to dtime
microsec = np.ceil(microsec * 1.0e6)
dtime += dt.timedelta(microseconds=int(microsec))
except ValueError as verr:
if(len(verr.args) > 0
and verr.args[0].startswith('unconverted data remains: ')):
vsplit = verr.args[0].split(" ")
dtime = dt.datetime.strptime(str_time[:-(len(vsplit[-1]))],
datetime_fmt)
else:
raise ValueError(verr)
return dtime
[docs]def deg2hr(lon):
"""Convert from degrees to hours.
Parameters
----------
lon : float or array-like
Longitude-like value in degrees
Returns
-------
lt : float or array-like
Local time-like value in hours
"""
lon = np.asarray(lon)
lt = lon / 15.0 # 12 hr/180 deg = 1/15 hr/deg
return lt
[docs]def hr2deg(lt):
"""Convert from degrees to hours.
Parameters
----------
lt : float or array-like
Local time-like value in hours
Returns
-------
lon : float or array-like
Longitude-like value in degrees
"""
lt = np.asarray(lt)
lon = lt * 15.0 # 180 deg/12 hr = 15 deg/hr
return lon
[docs]def hr2rad(lt):
"""Convert from hours to radians.
Parameters
----------
lt : float or array-like
Local time-like value in hours
Returns
-------
lon : float or array-like
Longitude-like value in radians
"""
lt = np.asarray(lt)
lon = lt * np.pi / 12.0
return lon
[docs]def rad2hr(lon):
"""Convert from radians to hours.
Parameters
----------
lon : float or array-like
Longitude-like value in radians
Returns
-------
lt : float or array-like
Local time-like value in hours
"""
lon = np.asarray(lon)
lt = lon * 12.0 / np.pi
return lt
[docs]def datetime2hr(dtime):
"""Calculate hours of day from datetime.
Parameters
----------
dtime : dt.datetime
Universal time as a timestamp
Returns
-------
uth : float
Hours of day, includes fractional hours
"""
uth = dtime.hour + dtime.minute / 60.0 \
+ (dtime.second + dtime.microsecond * 1.0e-6) / 3600.0
return uth
[docs]def slt2glon(slt, dtime):
"""Convert from solar local time to geographic longitude.
Parameters
----------
slt : float or array-like
Solar local time in hours
dtime : dt.datetime
Universal time as a timestamp
Returns
-------
glon : float or array-like
Geographic longitude in degrees
"""
# Calculate universal time of day in hours
uth = datetime2hr(dtime)
# Calculate the longitude in degrees
slt = np.asarray(slt)
glon = hr2deg(slt - uth)
# Ensure the longitude is not at or above 360 or at or below -180
glon = fix_range(glon, -180.0, 360.0, 360.0)
return glon
[docs]def glon2slt(glon, dtime):
"""Convert from geographic longitude to solar local time.
Parameters
----------
glon : float or array-like
Geographic longitude in degrees
dtime : dt.datetime
Universal time as a timestamp
Returns
-------
slt : float or array-like
Solar local time in hours
"""
# Calculate the longitude in degrees
slt = deg2hr(glon) + datetime2hr(dtime)
# Ensure the local time is between 0 and 24 h
slt = fix_range(slt, 0.0, 24.0)
return slt
[docs]def fix_range(values, min_val, max_val, val_range=None):
"""Ensure cyclic values lie below the maximum and at or above the mininum.
Parameters
----------
values : int, float, or array-like
Values to adjust
min_val : int or float
Maximum that values may not meet or exceed
max_val : int or float
Minimum that values may not lie below
val_range : int, float, or NoneType
Value range or None to calculate from min and max (default=None)
Returns
-------
fixed_vals : int, float, or array-like
Values adjusted to lie min_val <= fixed_vals < max_val
"""
# Cast output as array-like
fixed_vals = np.asarray(values)
# Test input to ensure the maximum is greater than the minimum
if min_val >= max_val:
raise ValueError('Minimum is not less than the maximum')
# Determine the allowable range
if val_range is None:
val_range = max_val - min_val
# Test input to ensure the value range is greater than zero
if val_range <= 0.0:
raise ValueError('Value range must be greater than zero')
# Fix the values, allowing for deviations that are multiples of the
# value range. Also propagate NaNs
ibad = (np.greater_equal(fixed_vals, max_val, where=~np.isnan(fixed_vals))
& ~np.isnan(fixed_vals))
while np.any(ibad):
fixed_vals[ibad] -= val_range
ibad = (np.greater_equal(fixed_vals, max_val,
where=~np.isnan(fixed_vals))
& ~np.isnan(fixed_vals))
ibad = (np.less(fixed_vals, min_val, where=~np.isnan(fixed_vals))
& ~np.isnan(fixed_vals))
while np.any(ibad):
fixed_vals[ibad] += val_range
ibad = (np.less(fixed_vals, min_val, where=~np.isnan(fixed_vals))
& ~np.isnan(fixed_vals))
return fixed_vals