pyephem sidereal time gives unexpected result - python

I'm using ephem for the first time, and having trouble understanding the output of oberver.sidereal_time()
I've written a couple scripts to determine solar time from hour angle. The first one uses ephem to compute right ascension and a formula from Meeus' Astronomical Algorithms to get Greenwich mean sidereal time, which can be converted to local mean sidereal with the longitude.
import sys
from datetime import datetime, time, timedelta
import ephem
def hour_angle(dt, longit, latit, elev):
obs = ephem.Observer()
obs.date = dt.strftime('%Y/%m/%d %H:%M:%S')
obs.lon = longit
obs.lat = latit
obs.elevation = elev
sun = ephem.Sun()
sun.compute(obs)
# get right ascention
ra = ephem.degrees(sun.g_ra)
# get sidereal time at greenwich (AA ch12)
jd = ephem.julian_date(dt)
t = (jd - 2451545.0) / 36525
theta = 280.46061837 + 360.98564736629 * (jd - 2451545) \
+ .000387933 * t**2 - t**3 / 38710000
# hour angle (AA ch13)
ha = (theta + longit - ra * 180 / ephem.pi) % 360
return ha
def main():
if len(sys.argv) != 6:
print 'Usage: hour_angle.py [YYYY/MM/DD] [HH:MM:SS] [longitude] [latitude] [elev]'
sys.exit()
else:
dt = datetime.strptime(sys.argv[1] + ' ' + sys.argv[2], '%Y/%m/%d %H:%M:%S')
longit = float(sys.argv[3])
latit = float(sys.argv[4])
elev = float(sys.argv[5])
# get hour angle
ha = hour_angle(dt, longit, latit, elev)
# convert hour angle to timedelta from noon
days = ha / 360
if days > 0.5:
days -= 0.5
td = timedelta(days=days)
# make solar time
solar_time = datetime.combine(dt.date(), time(12)) + td
print solar_time
if __name__ == '__main__':
main()
This gives output I would expect when I plug in some data:
> python hour_angle_ephem.py 2012/11/16 20:34:56 -122.2697 37.8044 3.0
2012-11-16 12:40:54.697115
The second script I wrote calculates right ascension the same way, but uses ephem's sidereal_time() to get the local apparent sidereal time.
import sys
from datetime import datetime, time, timedelta
import math
import ephem
def solartime(observer, sun=ephem.Sun()):
sun.compute(observer)
# sidereal time == ra (right ascension) is the highest point (noon)
t = observer.sidereal_time() - sun.ra
return ephem.hours(t + ephem.hours('12:00')).norm # .norm for 0..24
def main():
if len(sys.argv) != 6:
print 'Usage: hour_angle.py [YYYY/MM/DD] [HH:MM:SS] [longitude] [latitude] [elev]'
sys.exit()
else:
dt = datetime.strptime(sys.argv[1] + ' ' + sys.argv[2], '%Y/%m/%d %H:%M:%S')
longit = float(sys.argv[3])
latit = float(sys.argv[4])
elev = float(sys.argv[5])
obs = ephem.Observer()
obs.date = dt.strftime('%Y/%m/%d %H:%M:%S')
obs.lon = longit
obs.lat = latit
obs.elevation = elev
solar_time = solartime(obs)
print solar_time
if __name__ == '__main__':
main()
This does not get me the output I would expect.
python hour_angle_ephem2.py 2012/11/16 20:34:56 -122.2697 37.8044 3.0
9:47:50.83
AFAIK, the only difference between the two scripts is that the first bases hour angle on local mean sidereal time, while the second bases hour angle on local apparent sidereal time, which takes into account the nutation of the earth, which I think should be a very small factor. Instead I'm seeing a difference of about three hours. Can anyone explain to me what is going on?

When you provide PyEphem with a raw floating-point number where it expects an angle, then it trusts that you have converted the angle to radians first — since it always treats floating point angles as radians, to keep things consistent. But in your second script, you are getting longitudes and latitudes that are expressed in degrees and providing them to PyEphem as though they are in radians. You can see the result if you add a print statement or two to see what the .lon and .lat attributes of your Observer look like:
print observer.lon #--> -7005:32:16.0
print observer.lat #--> 2166:01:57.2
I think that what you instead want to do is simply provide your raw longitude and latitude strings to PyEphem so that it interprets them as human-readable degrees instead of machine-readable radians, by removing the float() calls around argv[3] and argv[4] in your second script. You should then find that it returns a value closer to what you are expecting:
$ python tmp11.py 2012/11/16 20:34:56 -122.2697 37.8044 3.0
12:40:55.59

Related

Using astropy to generate solar eclipse conditions based on my location

This is a question for the astronomy-minded folks on here.
I am an amateur astrophotographer looking to develop a personal script to aid my photography of next year's total solar eclipse. I am developing a Python script to automate my photography, so that I may enjoy the eclipse with my own eyes while my DSLR clicks away. Here's the script I've developed so far. The script uses digicamcontrol to control the camera.
Right now in the script I have just develop automation based on the partial phase of the eclipse (first contact, C1) and the timing of the eclipse in UTC (as well as my own PC). But a thought occurred to me: What if I can't connect to the internet and get the exact timing of the solar eclipse based on my location? I'd like to be able to generate those times. Is there a more efficient method to utilize astropy for this task? Thanks in advance.
import digiCamControlPython as dccp
import time
from datetime import datetime
from astropy.time import Time
local_time = Time.now()
utc_time_now = local_time.utc
def PartialEclipse(start_time:str, end_time:str):
camera = dccp.Camera()
camera.setIso(100)
camera.setShutterspeed("1/50")
camera.setFolder(r"C:\Users\My_Name\Pictures\digiCamControl")
# Set the target capture time in astropy time format
partial_eclipse_start = Time(start_time, format='isot')
partial_eclipse_end = Time(end_time, format='isot')
# Wait until the capture time
while utc_time_now < partial_eclipse_start:
time.sleep(1)
# Start capturing images
while utc_time_now < partial_eclipse_end:
camera.capture()
time.sleep(30) # Capture an image every 30 seconds
PartialEclipse("2024-04-08T17:12:13", "2024-04-08T18:29:24") #times of partial eclipse start and T-15s before totality
EDIT: In the event anyone ever looks at this question, I did make some progress on this.
import numpy as np
import astropy.units as u
from astropy.coordinates import solar_system_ephemeris, AltAz, EarthLocation, SkyCoord
from astropy.coordinates import get_body, get_moon, get_sun
from astropy.time import Time
myLocation = EarthLocation(lat=26*u.deg, lon=-80*u.deg, height=0*u.m)
# set the time step (how often to check for a solar eclipse in seconds)
time_step = 3600 # 1 hour
# set the number of days to check for a solar eclipse
num_days = 365
# set the start and end times to check for a solar eclipse
start_time = Time.now()
end_time = start_time + num_days * u.day
# initialize a list to store the times of a solar eclipse
eclipse_times = []
# loop over the desired time range, checking for a solar eclipse every time_step seconds
with solar_system_ephemeris.set('jpl'):
for t in np.arange(start_time.unix, end_time.unix, time_step):
time = Time(t, format='unix')
moon = get_body('moon', time, myLocation)
sun = get_body('sun', time, myLocation)
sun_coord = SkyCoord(sun.ra, sun.dec, sun.distance, frame='icrs')
moon_coord = SkyCoord(moon.ra, moon.dec, moon.distance, frame='icrs')
# check if the angular separation between the moon and sun is close to zero
angular_separation = moon_coord.separation(sun_coord)
if angular_separation < 0.6 * u.deg: #elongation where the partial eclipse begins
eclipse_times.append(time)
# print the times of the next solar eclipse
if len(eclipse_times) > 0:
print("The next solar eclipse is at: ", eclipse_times[0].iso)
else:
print("No solar eclipses found in the specified time range.")
I think your approach is really good! If you want to increase the accuracy of your start time prediction without using a lot more computational power, you can use scipy.optimize.root_scalar to refine the start time you found.
In my solution below, I've defined a function called distance_contact() whose root represents the start of the eclipse. This function is zero if the Sun and Moon are barely touching, positive if they are separated, and negative if they are overlapping. Then I define a grid of times with a timestep of 1 hour similar to your code, and pass it into this function to search for eclipses. It then finds the first time where distance_contanct is negative and uses that time and the timestep before as the search space for scipy.optimize.root_scalar.
Also, instead of using 0.6 * u.deg as the separation distance for an eclipse to occur, I've calculated the angular radius of the Sun and Moon for the time argument to distance_contact to make the prediction as accurate as possible.
import numpy as np
import scipy.optimize
import astropy.units as u
import astropy.time
import astropy.constants
import astropy.coordinates
def distance_contact(
location: astropy.coordinates.EarthLocation,
time: astropy.time.Time,
eclipse_type: str,
) -> u.Quantity:
radius_sun = astropy.constants.R_sun
radius_moon = 1737.4 * u.km
coordinate_sun = astropy.coordinates.get_sun(time)
coordinate_moon = astropy.coordinates.get_moon(time)
frame_local = astropy.coordinates.AltAz(obstime=time, location=location)
alt_az_sun = coordinate_sun.transform_to(frame_local)
alt_az_moon = coordinate_moon.transform_to(frame_local)
angular_radius_sun = np.arctan2(radius_sun, alt_az_sun.distance).to(u.deg)
angular_radius_moon = np.arctan2(radius_moon, alt_az_moon.distance).to(u.deg)
if eclipse_type == 'total':
separation_max = angular_radius_moon - angular_radius_sun
elif eclipse_type == 'partial':
separation_max = angular_radius_moon + angular_radius_sun
else:
raise ValueError("Unknown eclipse type")
return (alt_az_moon.separation(alt_az_sun).deg * u.deg) - separation_max
def calc_time_start(
location: astropy.coordinates.EarthLocation,
time_search_start: astropy.time.Time,
time_search_stop: astropy.time.Time,
eclipse_type: str = 'partial'
) -> astropy.time.Time:
astropy.coordinates.solar_system_ephemeris.set("de430")
# If we're only looking for a partial eclipse, we can accept a coarser search grid
if eclipse_type == "partial":
step = 1 * u.hr
elif eclipse_type == "total":
step = 1 * u.min
else:
raise ValueError("Unknown eclipse type")
# Define a grid of times to search for eclipses
time = astropy.time.Time(np.arange(time_search_start, time_search_stop, step=step))
# Find the times that are during an eclipse
mask_eclipse = distance_contact(location=location, time=time, eclipse_type=eclipse_type) < 0
# Find the index of the first time that an eclipse is occuring
index_start = np.argmax(mask_eclipse)
# Search around that time to find when the eclipse actually starts
time_eclipse_start = scipy.optimize.root_scalar(
f=lambda t: distance_contact(location, astropy.time.Time(t, format="unix"), eclipse_type=eclipse_type).value,
bracket=[time[index_start - 1].unix, time[index_start].unix],
).root
time_eclipse_start = astropy.time.Time(time_eclipse_start, format="unix")
return time_eclipse_start
def test_calc_time_start():
location = astropy.coordinates.EarthLocation(lat=26 * u.deg, lon=-80 * u.deg, height=0 * u.m)
eclipse_type = 'partial'
time_start = calc_time_start(
location=location,
time_search_start=astropy.time.Time.now(),
time_search_stop=astropy.time.Time.now() + 0.9 * u.yr,
eclipse_type=eclipse_type,
)
print(time_start.isot)
which outputs:
2023-10-14T15:57:38.068

Parsing and converting output from a GPS Dongle

I bought a cheap ublox7 GPS dongle and stuck it on my raspberry pi 3. When I looked at the output and tried to stick it into a map program I got weird results. Here is some sample output from the device after parsing with a library called "pynmea2".
$GPGLL,3745.81303,N,12214.62049,W,175033.00,A,D*7C
I did some research about how to convert this output to something useful and I found a formula that involved splitting the number up and dividing it by .6.
Doing GPS Conversion – Degrees to Latitude Longitude and vice versa
so I wrote a python program to try to capture and convert all of this, and the output is off by like a mile. I am trying to figure out what I am doing wrong, how could I be this close yet still off by about one mile?
from time import sleep
import pynmea2
import serial
import re
degree_sign = u"\N{DEGREE SIGN}"
ser = serial.Serial('/dev/ttyACM0', 9600, timeout=1.0)
sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))
while True:
line = sio.readline()
msg = pynmea2.parse(line)
msg = str(msg)
if re.search("GPGLL", msg):
raw_nums = re.findall(r'\b\d*\.\d*', msg)
lat_whole = (raw_nums[0])
lat_part1 = lat_whole[0:2]
lat_part2 = lat_whole[2:4]
lat_part2 = int(lat_part2)
lat_part2 = lat_part2 / .6
lat_part2 = int(lat_part2)
lat_part2 = str(lat_part2)
lat_part3 = lat_whole[5:9]
lat_part3 = float(lat_part3)
lat_part3 = lat_part3 / .6
lat_part3 = round(lat_part3, 0)
lat_part3 = int(lat_part3)
lat_part3 = str(lat_part3)
lon_whole = raw_nums[1]
lon_part1 = lon_whole[0:3]
lon_part1 = int(lon_part1)
lon_part1 = -lon_part1
lon_part1 = str(lon_part1)
lon_part2 = lon_whole[3:5]
lon_part2 = int(lon_part2)
lon_part2 = lon_part2 / .6
lon_part2 = str(lon_part2)
lon_part2 = lon_part2[0:2]
lon_part3 = lon_whole[6:10]
lon_part3 = float(lon_part3)
lon_part3 = lon_part3 / .6
lon_part3 = round(lon_part3, 0)
lon_part3 = int(lon_part3)
lon_part3 = str(lon_part3)
print(lat_part1 + "." + lat_part2 + lat_part3 +"," , lon_part1 + "." + lon_part2 + lon_part3)
print(lat_part1+degree_sign+lat_part2+"'"+lat_part3+"\"" + "N", lon_part1 + degree_sign+ lon_part2 + "'" + lon_part3+"\"" + "W")
sleep(1)
Here is the list that regex generated using the pynmea2 output:
['3745.81246', '12214.61512', '224329.00'] assigned to raw_nums.
Output from the script:
37.7513540, -122.2310268
37°75'13540"N -122°23'10268"W
Entering the first bit of output into google maps brings up a place near me but about a mile away, the second number doesn't work on Google maps for some reason - but it works on apple maps.
My questions:
I know there must be at least 100 better ways to write this code, do you have suggestions for getting there quicker?
Does the formula make sense? Am I applying it correctly?
Do you see a reason why this should return a result that is close but no cigar?
Do you know why the second line of output would not work as input into google maps?
What accuracy should I expect from a ublox 7 GPS dongle I got from Amazon for $12?
Thanks in advance, I really appreciate it.
Update: I looked up my address on gps coordinates conversion
and the latitude they show for my address is 3745.50084 while my gps is reporting 3745.81246. So it just seems like I am starting with bad data...
If your parsed string from the GPS device is always of the form you specified, you can simply split the string on the commas like split_msg = msg.split(","). Then your lat will be split_msg[1] and your long split_msg[3]. With indexes 2 and 4 being the heading direction.
The lat is provided as DDmm.mm and long is provided as DDDmm.mm, which you seem to have captured above. So 3745.81246 would be 37 degrees and 45.81246 minutes. You can take the decimal portion of the minutes (i.e. 0.81246) and multiply times 60 to get seconds. So you would get 37 degrees, 45 minutes, and 48.75 seconds. As a sanity check, minutes and seconds should always be less than 60 as either of them being 60 would increment the next value (e.g. 60 minutes in a degree, 60 seconds in a minute).
To convert the minutes to a decimal degree number, simply divide the minutes number by 60 (45.81246/60=0.763541) then add that to your degrees. So 3745.81246 would become 37.763541.
So within the if statement:
split_msg = msg.split(",")
lat, lat_dir, long, long_dir = split_msg[1:5]
lat_d, lat_m = float(lat[:2]), float(lat[2:])
long_d, long_m = float(long[:3]), float(long[3:])
lat_dec = lat_d + lat_m/60
long_dec = long_d + long_m/60
lat_min = math.floor(lat_m)
lat_sec = 60*(lat_m - lat_min)
long_min = math.floor(long_m)
long_sec = 60*(long_m - long_min)
print(f"{lat_dec} {lat_dir}, {long_dec} {long_dir}")
print(f"{lat_d}{degree_sign} {lat_min}' {lat_sec}\" {lat_dir}, {long_d}{degree_sign} {long_min}' {long_sec}\" {long_dir}")
I have not tested the above code, but this is the general way I would approach this problem.
Since you're using the pynmea2 library you can make use of the object properties to ease the subsequent steps.
import pynmea2
line = "$GPGLL,3745.81303,N,12214.62049,W,175033.00,A,D*7C"
nmeaobj = pynmea2.parse(line)
coord = f'{nmeaobj.latitude} {nmeaobj.longitude}'
print(coord)
# 37.763551 -122.243675
The decimal degree difference between the script output and the library is around 0.012 which is similar to the precision length (1.1132 km) cited in the Degree precision versus length table. This would explain why you are seen a discrepancy of about a mile.
abs(-122.243675 - -122.2310268)
0.012648200000000998
abs(37.7635505 - 37.7513540)
0.01219650000000172
You could use the formula cited in the previous link to convert from decimal degree to DMS components, and this would yield a valid location. But notice that the directions of the coordinate (NS/WE) were left out of the final string formation.
def dd_to_dms(coord):
d = int(coord)
abs_d = abs(coord-d)
m = int(60 * abs_d)
s = 3600 * abs_d - 60 * m
return d,m,s
lat = '''%02d°%02d'%07.4f"''' % dd_to_dms(nmeaobj.latitude)
lon = '''%02d°%02d'%07.4f"''' % dd_to_dms(nmeaobj.longitude)
print(f'{lat} {lon}')
# 37°45'48.7818" -122°14'37.2294"
The second line of output would not work as input into google maps because, as already mentioned, minutes and seconds should always be less than 60. Moreover, adding the directions of the coordinate to a negative degree could make you (depending on the algorithm used to parse the string) "walk" in the opposite direction of the desired location as a consequence of having the degree sign (or the coordinate direction NS/WE) ignored, or, in Google maps case, simply not understanding the coordinate.
37°45'48.7818" -122°14'37.2294" # works
37°45'48.7818"N -122°14'37.2294"W # don't work
37°45'48.7818"N 122°14'37.2294"W # works

How to sum strings of hours and minutes without the use of any date or time libraries

I should not use any date or time libraries! Actually I was able to sum them as normal integers but I couldn't find how to sum them as hours and minutes so as an example "03:50" + "04:20" should be "08:10" but actually I got "7:70" because I still couldn't find valid approach. Any hint or approach is much appreciated. Thank you.
import re
def add_time(time, duration):
elems1 = re.split(r"\s", time)
elems2 = re.split(r":", elems1[0])
elems2.append(elems1[1])
elems3 = re.split(r":", duration)
resH = eval(elems2[0]) + eval(elems3[0])
resM = eval(elems2[1]) + eval(elems3[1])
return f'{str(resH)}:{str(resM)} {elems2[2]}'
print(add_time("3:50 AM", "4:20"))
Actual output
7:70 AM
Desired output
08:10 AM
You can implement it by yourself by using % to keep track of hours and minutes format, but keep in mind that the remainder of the minutes, if came across 60, should passed to the hours.
take a look in this implementation:
import re
import numpy as np
def add_time(time, duration):
elems1 = re.split(r"\s", time)
elems2 = re.split(r":", elems1[0])
elems2.append(elems1[1])
elems3 = re.split(r":", duration)
remainder_from_minutes = int(np.floor((int(elems2[1]) + int(elems3[1]))/60))
resH = (int(elems2[0]) + int(elems3[0]))%24 + remainder_from_minutes
resM = (int(elems2[1]) + int(elems3[1]))%60
am_or_pm = elems2[2] if int(resH/12)%2==0 else chr(145-ord(elems2[2][0])) + "M"
return str(resH).rjust(2,"0") + ":" + str(resM).rjust(2,"0") + " " + str(am_or_pm)
print(add_time("3:50 AM", "8:50"))
print(add_time("3:20 PM", "8:50"))
output:
12:40 PM
12:10 AM
fixed AM/PM notation as well, with some ascii table tricks, in such way that also 12:40 AM and 03:50 PM will be acceptable
i think you should add just condition for handling when minutes is greater than 59 and when hour is greater than 12.
import re
def add_time(time, duration):
elems1 = re.split(r"\s", time)
elems2 = re.split(r":", elems1[0])
elems2.append(elems1[1])
elems3 = re.split(r":", duration)
cf = 0
resM = eval(elems2[1]) + eval(elems3[1])
if (resM > 60):
cf = 1
resM = resM - 60
resH = eval(elems2[0]) + eval(elems3[0]) + cf
if(resH > 12):
resH = resH - 12
return f'{str(resH)}:{str(resM)} {elems2[2]}'
I think a good way to do this is to convert everything to minutes, sum them and convert them to hours + minutes back. This is good because this way you will be able to scale your system in future, for example, make it possible to add more than just two time values (which is not possible in one of the other answers). Because you are using Python which has built-in long arithmetic I don't see any disadvantages of this solution

How to check if relativedelta object is negative

I have some problem with using relativedelta objects - cannot determine if "delta is negative". What I'm trying is:
from dateutil.relativedelta import relativedelta
print relativedelta(seconds=-5) > 0
that gives me True which is counter intuitive.
print relativedelta(seconds=5) > 0
also return True. Is there a way to check if "delta" represented by relativedata object is negative?
I'm currently using a workaround in the form of separate function to check if delta is negative but I expected that there is more elegant solution. Here is code I'm using:
def is_relativedelta_positive(rel_delta):
is_positive = True
is_positive &= rel_delta.microseconds >= 0
is_positive &= rel_delta.seconds >= 0
is_positive &= rel_delta.minutes >= 0
is_positive &= rel_delta.hours >= 0
is_positive &= rel_delta.days >= 0
return is_positive
TL;DR There is not a clear and intuitive definition of comparison between relativedelta objects, so comparison is not implemented in dateutil. If you want to compare them, you'll need to make an arbitrary choice about the ordering.
The problem
The semantics of comparisons between relativedelta are undefined because relativedelta objects themselves don't represent a fixed period of time. You can see this issue on github as to why this is a problem.
There are two major problems with comparisons between relativedelta objeccts. The more straightforward one is that relativedelta has "absolute" components (the singular arguments) such as day, hour, etc. So consider:
from dateutil.relativedelta import relativedelta
from datetime import datetime
rd1 = relativedelta(day=5, hours=5)
rd2 = relativedelta(hours=8)
for i in range(4, 7):
dt = datetime(2014, 1, i)
print((dt + rd1) > (dt + rd2))
# Result:
# True
# False
# False
Since each relativedelta does not represent a fixed amount of time, it's not necessarily meaningful to compare which one is "bigger" or "smaller" than the other.
The other problem is that even if you restrict yourself to the "relative" components of the relativedelta, all units larger than week depend on what they are being added to, so:
rd3 = relativedelta(months=1)
rd4 = relativedelta(days=30)
for i in range(1, 4):
dt = datetime(2015, i, 1)
print((dt + rd3) > (dt + rd4))
# Result:
# True
# False
# True
Possible comparison operations
That said, there are a few possible definitions that you can meaningfully use if you want a semi-arbitrary but consistent definition of "less than" for relativedelta.
One somewhat limited version of this is to say that "absolute" components will throw an error and to set a fixed value for the "relative" components:
def rd_to_td(rd):
for comp in ['year', 'month', 'day', 'hour', 'minute', 'second',
'microsecond', 'weekday', 'leapdays']:
if getattr(rd, comp) is not None:
raise ValueError('Conversion not supported with component ' + comp)
YEAR_LEN = 365.25
MON_LEN = 30
days = (rd.years or 0) * YEAR_LEN
days += (rd.months or 0) * MON_LEN
return timedelta(days=days, hours=rd.hours, minutes=rd.minutes,
seconds=rd.seconds, microseconds=rd.microseconds)
Closest to universal comparison
The above works for limited cases, but probably most universal comparison method you can define is to simply add both to a fixed date and compare the results:
from datetime import datetime
def lt_at_dt(rd1, rd2, dt=datetime(1970, 1, 1)):
return (dt + rd1) < (dt + rd2)
If you want this as a key for sorting (rather than for pairwise comparisons), this same definition of "less than" can be used to convert relativedelta to timedelta (which is a fixed period of time):
def rd_to_td_at_dt(rd, dt=datetime(1970, 1, 1)):
return (dt + rd1) - dt
Note The previous two definitions are about the more general operation of comparison between relativedelta objects. To know if one of these is negative, just compare the result to a relativedelta representing zero, or convert to timedelta by one of the above methods and compare to timedelta(0).
Finally, I will note that in the forthcoming 2.7.0 release of dateutil, relativedelta will define __abs__ (GH PR #472), so your original definition of positivity can be reduced to abs(rd) == rd. However, as Martijn points out, abs(relativedelta(days=20, hours=-1)) != relativedelta(days=20, hours=-1), but by most reasonable definitions, that relative delta is always a positive offset.
relativedelta() objects do not implement the necessary comparison methods. In Python 2 that means that they are thus compared by their type name, and numbers are always sorted before any other objects; this makes these objects larger than integer values whatever their values. In Python 3 you'd get a TypeError instead.
Your work-around doesn't account for an absolute positive value, relativedelta(years=1, seconds=-5) would move your datetime by almost a whole year forward, so it could hardly be named a negative delta.
You'd have to compare individual attributes instead (so years, months, days, hours, minutes, seconds and microseconds). Depending on your use-case, you may have to convert those to a total number of seconds:
def total_seconds(rd, _yeardays=365.2425, _monthdays=365.2425/12):
"""approximation of the number of seconds in a relative delta"""
# year and month durations are averages, taking into account leap years
total_days = rd.years * _yeardays + (rd.months * _monthdays) + rd.days
total_hours = total_days * 24 + rd.hours
total_minutes = total_hours * 60 + rd.minutes
return total_minutes * 60 + rd.seconds + (rd.microseconds / 1000000)
then use this to do your comparisons:
if total_seconds(relativedelta(seconds=-5)) > 0:
The total_seconds() function produces an approximation; relative deltas handle leap years and the right number of days per month, so their actual effect on a datetime object will vary depending on that datetime value. However, the above should be good enough for the majority of cases. It does completely ignore the absolute components of the relative delta (hour, year, the singular names, that state a fixed value rather than a delta).
An elegant and concise way of checking whether a relativedelta object is negative:
from datetime import datetime
from dateutil.relativedelta import relativedelta
def is_negative(rd: relativedelta) -> bool:
''' Check whether a relativedelta object is negative'''
try:
datetime.min + rd
return False
except OverflowError:
return True
Some examples:
is_negative(relativedelta(hours=1))
>> False
is_negative(relativedelta(hours=0))
>> False
is_negative(relativedelta(hours=-1))
>> True
is_negative(relativedelta(days=1, hours=-1))
>> False
is_negative(relativedelta(days=-1, hours=1))
>> True

Keeping a strange time format, and adding values to it in python

So i have been trying to add a time format to my REST calls in python, but there seems to always be some type of issue, first of all here is the time format requirement, and it has to be exact, or it wont work unfortunately.
Use the following ISO-8601 compliant date/time format in request parameters.
yyyy-MM-dd'T'HH:mm:ss.SSSXXX
For example, May 26 2014 at 21:49:46 PM could have a format like one of the following:
l In PDT: 2014-05-26T21:49:46.000-07:00
l In UTC: 2014-05-26T21:49:46.000Z
Code Description
yyyy Four digit year
MM Two-digit month (01=January, etc.)
dd Two-digit day of month (01 through 31)
T Separator for date/time
HH Two digits of hour (00 through 23) (am/pm NOT allowed)
mm Two digits of minute (00 through 59)
ss Two digits of second (00 through 59)
SSS Three digit milliseconds of the second
XXX ISO 8601 time zone (Z or +hh:mm or -hh:mm)
So, what i have tried before is:
def format_time(self, isnow):
currentdt = datetime.datetime.utcnow()
if not isnow:
currentdt += datetime.timedelta(0,3)
(dt, micro) = currentdt.strftime('%Y-%m-%dT%H:%M:%S.%f').split('.')
dt = "%s.%03dZ" % (dt, int(micro) / 1000)
return dt
Now, this might return it in the kinda right format, but there is still the problem with timezones.
The end result i am trying to accomplish, is when i execute this, it finds the current time, (Amsterdam timezone/GMT/UTC+1), and creates it in this format.
And the else statement, to get the same time, but append X seconds.
Would anyone be so kind to help me out here?
Ok, so you got the microseconds formatted as milliseconds, well done there.
Now your challenge is to handle the timezone offset; it can't only be Z.
And to make things more difficult, strftime's %z format gives + (or -) HHMM, instead of HH:MM.
So you'll need to deal with that. Here's one way to do it:
Python 3:
def format_time(self, isnow):
currentdt = datetime.datetime.now(datetime.timezone.utc)
if not isnow:
currentdt += datetime.timedelta(0,3)
(dt, micro) = currentdt.strftime('%Y-%m-%dT%H:%M:%S.%f').split('.')
tz_offset = currentdt.astimezone().strftime('%z')
tz_offset = "Z" if tz_offset == "" else tz_offset[:3] + ":" + tz_offset[3:]
dt = "%s.%03d%s" % (dt, int(micro) / 1000, tz_offset)
return dt
Python 2:
import pytz
from dateutil.tz import *
def format_time(self, isnow):
currentdt = datetime.datetime.now(pytz.utc)
if not isnow:
currentdt += datetime.timedelta(0,3)
(dt, micro) = currentdt.strftime('%Y-%m-%dT%H:%M:%S.%f').split('.')
tz_offset = currentdt.astimezone(tzlocal()).strftime('%z')
tz_offset = "Z" if tz_offset == "" else tz_offset[:3] + ":" + tz_offset[3:]
dt = "%s.%03d%s" % (dt, int(micro) / 1000, tz_offset)
return dt
Response to comment:
I needed to make a few changes. It's remarkably non-trivial to find the current timezone. The easiest way I could find was from https://stackoverflow.com/a/25887393/1404311 and I've integrated those concepts into the code that is now above.
Basically, instead of utcnow(), you should use now(datetime.timezone.utc). The former gives a naive datetime, while the latter gives a datetime set to UTC, but aware that it is. Then use astimezone() to make it aware of your local timezone, then use strftime('%z') to get the time offzone from there. THEN go through the string manipulation.

Categories