How to check if relativedelta object is negative - python

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

Related

Python difference in datetime now against datetime now

I have got into an issue or might quite possibly feature in turn! Not sure, wondering!! In python's datetime library, to get difference in time, as in below snippet.
>>> import datetime
>>> datetime.datetime.now() - datetime.datetime.now()
datetime.timedelta(-1, 86399, 999958)
>>> tnow = datetime.datetime.now()
>>> datetime.datetime.now() - tnow
datetime.timedelta(0, 4, 327859)
I would like to understand why datetime.datetime.now() - datetime.datetime.now() is producing output as -1 days, 86399 seconds whereas assigning current time to some variable and computing difference gives desired output 0 days, 4 seconds.
The results seems to be bit confusing, it would be helpful if someone could decode whats going behind
Note: I'm using Python 2.7
As per the documentation of timedelta object
If the normalized value of days lies outside the indicated range,
OverflowError is raised.
Note that normalization of negative values may be surprising at first.
For example:
>>> from datetime import timedelta
>>> d = timedelta(microseconds=-1)
>>> (d.days, d.seconds, d.microseconds)
(-1, 86399, 999999)
This is valid for python 2.7 and 3 both.
Why this is happening is simple:
a , b = datetime.datetime.now(), datetime.datetime.now()
# here datetime.now() in a will be <= b.
# That is because they will be executed separately at different CPU clock cycle.
a - b
# datetime.timedelta(-1, 86399, 999973)
b - a
# datetime.timedelta(0, 0, 27)
To get the proper time difference:
(tnow - datetime.datetime.now()).total_seconds()
# output: -1.751166
This Answer gives more information on how to use time delta safely (handle negative values) Link
You are encountering a "corner case" situation.
Every datetime.datetime.now() produces a datetime.datetime object ([Python]: https://docs.python.org/3/library/datetime.html#datetime-objects), which is the current date & time at the moment the call was made
You have 2 such calls (even if they are on the same line). Since the CPU speeds are very high nowadays, every such call takes a very small amount of time (much less than microseconds, I presume)
But, when the 1st call is at the very end of a (microsecond?) period, and the 2nd one is at the beginning of the next one, you'd get this behavior:
>>> import datetime
>>> now0 = datetime.datetime.now()
>>> now0
datetime.datetime(2018, 2, 20, 12, 23, 23, 1000)
>>> delta = datetime.timedelta(microseconds=1)
>>> now1 = now0 + delta
>>> now0 - now1
datetime.timedelta(-1, 86399, 999999)
Explanation:
Let now0 to be the result of the 1st call made to datetime.datetime.now()
Let's say that the 2nd datetime.datetime.now() call happens one microsecond later (I am reproducing the behavior using the delta object, as the times involved here are waaay too small for me to be able to to run the line at the exact time when this behavior is encountered). That is placed into now1
When subtracting them you get the negative value (in my case is -delta), since now0 happened earlier than now1 (check [Python]: timedelta Objects for more details)

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.

Is there any proper method to convert ISO8601(with tzone) to milliseconds on python? [duplicate]

This question already has answers here:
How do I parse an ISO 8601-formatted date?
(29 answers)
Closed 8 years ago.
The community reviewed whether to reopen this question last month and left it closed:
Original close reason(s) were not resolved
I'm getting a datetime string in a format like "2009-05-28T16:15:00" (this is ISO 8601, I believe). One hackish option seems to be to parse the string using time.strptime and passing the first six elements of the tuple into the datetime constructor, like:
datetime.datetime(*time.strptime("2007-03-04T21:08:12", "%Y-%m-%dT%H:%M:%S")[:6])
I haven't been able to find a "cleaner" way of doing this. Is there one?
I prefer using the dateutil library for timezone handling and generally solid date parsing. If you were to get an ISO 8601 string like: 2010-05-08T23:41:54.000Z you'd have a fun time parsing that with strptime, especially if you didn't know up front whether or not the timezone was included. pyiso8601 has a couple of issues (check their tracker) that I ran into during my usage and it hasn't been updated in a few years. dateutil, by contrast, has been active and worked for me:
from dateutil import parser
yourdate = parser.parse(datestring)
Since Python 3.7 and no external libraries, you can use the fromisoformat function from the datetime module:
datetime.datetime.fromisoformat('2019-01-04T16:41:24+02:00')
Python 2 doesn't support the %z format specifier, so it's best to explicitly use Zulu time everywhere if possible:
datetime.datetime.strptime("2007-03-04T21:08:12Z", "%Y-%m-%dT%H:%M:%SZ")
Because ISO 8601 allows many variations of optional colons and dashes being present, basically CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]. If you want to use strptime, you need to strip out those variations first.
The goal is to generate a UTC datetime object.
If you just want a basic case that work for UTC with the Z suffix like 2016-06-29T19:36:29.3453Z:
datetime.datetime.strptime(timestamp.translate(None, ':-'), "%Y%m%dT%H%M%S.%fZ")
If you want to handle timezone offsets like 2016-06-29T19:36:29.3453-0400 or 2008-09-03T20:56:35.450686+05:00 use the following. These will convert all variations into something without variable delimiters like 20080903T205635.450686+0500 making it more consistent/easier to parse.
import re
# This regex removes all colons and all
# dashes EXCEPT for the dash indicating + or - utc offset for the timezone
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', timestamp)
datetime.datetime.strptime(conformed_timestamp, "%Y%m%dT%H%M%S.%f%z" )
If your system does not support the %z strptime directive (you see something like ValueError: 'z' is a bad directive in format '%Y%m%dT%H%M%S.%f%z') then you need to manually offset the time from Z (UTC). Note %z may not work on your system in Python versions < 3 as it depended on the C library support which varies across system/Python build type (i.e., Jython, Cython, etc.).
import re
import datetime
# This regex removes all colons and all
# dashes EXCEPT for the dash indicating + or - utc offset for the timezone
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', timestamp)
# Split on the offset to remove it. Use a capture group to keep the delimiter
split_timestamp = re.split(r"([+|-])",conformed_timestamp)
main_timestamp = split_timestamp[0]
if len(split_timestamp) == 3:
sign = split_timestamp[1]
offset = split_timestamp[2]
else:
sign = None
offset = None
# Generate the datetime object without the offset at UTC time
output_datetime = datetime.datetime.strptime(main_timestamp +"Z", "%Y%m%dT%H%M%S.%fZ" )
if offset:
# Create timedelta based on offset
offset_delta = datetime.timedelta(hours=int(sign+offset[:-2]), minutes=int(sign+offset[-2:]))
# Offset datetime with timedelta
output_datetime = output_datetime + offset_delta
Arrow looks promising for this:
>>> import arrow
>>> arrow.get('2014-11-13T14:53:18.694072+00:00').datetime
datetime.datetime(2014, 11, 13, 14, 53, 18, 694072, tzinfo=tzoffset(None, 0))
Arrow is a Python library that provides a sensible, intelligent way of creating, manipulating, formatting and converting dates and times. Arrow is simple, lightweight and heavily inspired by moment.js and requests.
You should keep an eye on the timezone information, as you might get into trouble when comparing non-tz-aware datetimes with tz-aware ones.
It's probably the best to always make them tz-aware (even if only as UTC), unless you really know why it wouldn't be of any use to do so.
#-----------------------------------------------
import datetime
import pytz
import dateutil.parser
#-----------------------------------------------
utc = pytz.utc
BERLIN = pytz.timezone('Europe/Berlin')
#-----------------------------------------------
def to_iso8601(when=None, tz=BERLIN):
if not when:
when = datetime.datetime.now(tz)
if not when.tzinfo:
when = tz.localize(when)
_when = when.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
return _when[:-8] + _when[-5:] # Remove microseconds
#-----------------------------------------------
def from_iso8601(when=None, tz=BERLIN):
_when = dateutil.parser.parse(when)
if not _when.tzinfo:
_when = tz.localize(_when)
return _when
#-----------------------------------------------
I haven't tried it yet, but pyiso8601 promises to support this.
import datetime, time
def convert_enddate_to_seconds(self, ts):
"""Takes ISO 8601 format(string) and converts into epoch time."""
dt = datetime.datetime.strptime(ts[:-7],'%Y-%m-%dT%H:%M:%S.%f')+\
datetime.timedelta(hours=int(ts[-5:-3]),
minutes=int(ts[-2:]))*int(ts[-6:-5]+'1')
seconds = time.mktime(dt.timetuple()) + dt.microsecond/1000000.0
return seconds
This also includes the milliseconds and time zone.
If the time is '2012-09-30T15:31:50.262-08:00', this will convert into epoch time.
>>> import datetime, time
>>> ts = '2012-09-30T15:31:50.262-08:00'
>>> dt = datetime.datetime.strptime(ts[:-7],'%Y-%m-%dT%H:%M:%S.%f')+ datetime.timedelta(hours=int(ts[-5:-3]), minutes=int(ts[-2:]))*int(ts[-6:-5]+'1')
>>> seconds = time.mktime(dt.timetuple()) + dt.microsecond/1000000.0
>>> seconds
1348990310.26
Both ways:
Epoch to ISO time:
isoTime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(epochTime))
ISO time to Epoch:
epochTime = time.mktime(time.strptime(isoTime, '%Y-%m-%dT%H:%M:%SZ'))
Isodate seems to have the most complete support.
aniso8601 should handle this. It also understands timezones, Python 2 and Python 3, and it has a reasonable coverage of the rest of ISO 8601, should you ever need it.
import aniso8601
aniso8601.parse_datetime('2007-03-04T21:08:12')
Here is a super simple way to do these kind of conversions.
No parsing, or extra libraries required.
It is clean, simple, and fast.
import datetime
import time
################################################
#
# Takes the time (in seconds),
# and returns a string of the time in ISO8601 format.
# Note: Timezone is UTC
#
################################################
def TimeToISO8601(seconds):
strKv = datetime.datetime.fromtimestamp(seconds).strftime('%Y-%m-%d')
strKv = strKv + "T"
strKv = strKv + datetime.datetime.fromtimestamp(seconds).strftime('%H:%M:%S')
strKv = strKv +"Z"
return strKv
################################################
#
# Takes a string of the time in ISO8601 format,
# and returns the time (in seconds).
# Note: Timezone is UTC
#
################################################
def ISO8601ToTime(strISOTime):
K1 = 0
K2 = 9999999999
K3 = 0
counter = 0
while counter < 95:
K3 = (K1 + K2) / 2
strK4 = TimeToISO8601(K3)
if strK4 < strISOTime:
K1 = K3
if strK4 > strISOTime:
K2 = K3
counter = counter + 1
return K3
################################################
#
# Takes a string of the time in ISO8601 (UTC) format,
# and returns a python DateTime object.
# Note: returned value is your local time zone.
#
################################################
def ISO8601ToDateTime(strISOTime):
return time.gmtime(ISO8601ToTime(strISOTime))
#To test:
Test = "2014-09-27T12:05:06.9876"
print ("The test value is: " + Test)
Ans = ISO8601ToTime(Test)
print ("The answer in seconds is: " + str(Ans))
print ("And a Python datetime object is: " + str(ISO8601ToDateTime(Test)))

datetime: Round/trim number of digits in microseconds

Currently I am logging stuff and I am using my own formatter with a custom formatTime():
def formatTime(self, _record, _datefmt):
t = datetime.datetime.now()
return t.strftime('%Y-%m-%d %H:%M:%S.%f')
My issue is that the microseconds, %f, are six digits. Is there anyway to spit out less digits, like the first three digits of the microseconds?
The simplest way would be to use slicing to just chop off the last three digits of the microseconds:
def format_time():
t = datetime.datetime.now()
s = t.strftime('%Y-%m-%d %H:%M:%S.%f')
return s[:-3]
I strongly recommend just chopping. I once wrote some logging code that rounded the timestamps rather than chopping, and I found it actually kind of confusing when the rounding changed the last digit. There was timed code that stopped running at a certain timestamp yet there were log events with that timestamp due to the rounding. Simpler and more predictable to just chop.
If you want to actually round the number rather than just chopping, it's a little more work but not horrible:
def format_time():
t = datetime.datetime.now()
s = t.strftime('%Y-%m-%d %H:%M:%S.%f')
head = s[:-7] # everything up to the '.'
tail = s[-7:] # the '.' and the 6 digits after it
f = float(tail)
temp = "{:.03f}".format(f) # for Python 2.x: temp = "%.3f" % f
new_tail = temp[1:] # temp[0] is always '0'; get rid of it
return head + new_tail
Obviously you can simplify the above with fewer variables; I just wanted it to be very easy to follow.
As of Python 3.6 the language has this feature built in:
def format_time():
t = datetime.datetime.now()
s = t.isoformat(timespec='milliseconds')
return s
This method should always return a timestamp that looks exactly like this (with or without the timezone depending on whether the input dt object contains one):
2016-08-05T18:18:54.776+0000
It takes a datetime object as input (which you can produce with datetime.datetime.now()). To get the time zone like in my example output you'll need to import pytz and pass datetime.datetime.now(pytz.utc).
import pytz, datetime
time_format(datetime.datetime.now(pytz.utc))
def time_format(dt):
return "%s:%.3f%s" % (
dt.strftime('%Y-%m-%dT%H:%M'),
float("%.3f" % (dt.second + dt.microsecond / 1e6)),
dt.strftime('%z')
)
I noticed that some of the other methods above would omit the trailing zero if there was one (e.g. 0.870 became 0.87) and this was causing problems for the parser I was feeding these timestamps into. This method does not have that problem.
An easy solution that should work in all cases:
def format_time():
t = datetime.datetime.now()
if t.microsecond % 1000 >= 500: # check if there will be rounding up
t = t + datetime.timedelta(milliseconds=1) # manually round up
return t.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
Basically you do manual rounding on the date object itself first, then you can safely trim the microseconds.
Edit: As some pointed out in the comments below, the rounding of this solution (and the one above) introduces problems when the microsecond value reaches 999500, as 999.5 is rounded to 1000 (overflow).
Short of reimplementing strftime to support the format we want (the potential overflow caused by the rounding would need to be propagated up to seconds, then minutes, etc.), it is much simpler to just truncate to the first 3 digits as outlined in the accepted answer, or using something like:
'{:03}'.format(int(999999/1000))
-- Original answer preserved below --
In my case, I was trying to format a datestamp with milliseconds formatted as 'ddd'. The solution I ended up using to get milliseconds was to use the microsecond attribute of the datetime object, divide it by 1000.0, pad it with zeros if necessary, and round it with format. It looks like this:
'{:03.0f}'.format(datetime.now().microsecond / 1000.0)
# Produces: '033', '499', etc.
You can subtract the current datetime from the microseconds.
d = datetime.datetime.now()
current_time = d - datetime.timedelta(microseconds=d.microsecond)
This will turn 2021-05-14 16:11:21.916229 into 2021-05-14 16:11:21
This method allows flexible precision and will consume the entire microsecond value if you specify too great a precision.
def formatTime(self, _record, _datefmt, precision=3):
dt = datetime.datetime.now()
us = str(dt.microsecond)
f = us[:precision] if len(us) > precision else us
return "%d-%d-%d %d:%d:%d.%d" % (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, int(f))
This method implements rounding to 3 decimal places:
import datetime
from decimal import *
def formatTime(self, _record, _datefmt, precision='0.001'):
dt = datetime.datetime.now()
seconds = float("%d.%d" % (dt.second, dt.microsecond))
return "%d-%d-%d %d:%d:%s" % (dt.year, dt.month, dt.day, dt.hour, dt.minute,
float(Decimal(seconds).quantize(Decimal(precision), rounding=ROUND_HALF_UP)))
I avoided using the strftime method purposely because I would prefer not to modify a fully serialized datetime object without revalidating it. This way also shows the date internals in case you want to modify it further.
In the rounding example, note that the precision is string-based for the Decimal module.
Here is my solution using regexp:
import re
# Capture 6 digits after dot in a group.
regexp = re.compile(r'\.(\d{6})')
def to_splunk_iso(dt):
"""Converts the datetime object to Splunk isoformat string."""
# 6-digits string.
microseconds = regexp.search(dt.isoformat()).group(1)
return regexp.sub('.%d' % round(float(microseconds) / 1000), dt.isoformat())
Fixing the proposed solution based on Pablojim Comments:
from datetime import datetime
dt = datetime.now()
dt_round_microsec = round(dt.microsecond/1000) #number of zeroes to round
dt = dt.replace(microsecond=dt_round_microsec)
If once want to get the day of the week (i.e, 'Sunday)' along with the result, then by slicing '[:-3]' will not work. At that time you may go with,
dt = datetime.datetime.now()
print("{}.{:03d} {}".format(dt.strftime('%Y-%m-%d %I:%M:%S'), dt.microsecond//1000, dt.strftime("%A")))
#Output: '2019-05-05 03:11:22.211 Sunday'
%H - for 24 Hour format
%I - for 12 Hour format
Thanks,
Adding my two cents here as this method will allow you to write your microsecond format as you would a float in c-style. It takes advantage that they both use %f.
import datetime
import re
def format_datetime(date, format):
"""Format a ``datetime`` object with microsecond precision.
Pass your microsecond as you would format a c-string float.
e.g "%.3f"
Args:
date (datetime.datetime): You input ``datetime`` obj.
format (str): Your strftime format string.
Returns:
str: Your formatted datetime string.
"""
# We need to check if formatted_str contains "%.xf" (x = a number)
float_format = r"(%\.\d+f)"
has_float_format = re.search(float_format, format)
if has_float_format:
# make microseconds be decimal place. Might be a better way to do this
microseconds = date.microsecond
while int(microseconds): # quit once it's 0
microseconds /= 10
ms_str = has_float_format.group(1) % microseconds
format = re.sub(float_format, ms_str[2:], format)
return date.strftime(format)
print(datetime.datetime.now(), "%H:%M:%S.%.3f")
# '17:58:54.424'

Is there any way to use a strftime-like function for dates before 1900 in Python?

I didn't realize this, but apparently Python's strftime function doesn't support dates before 1900:
>>> from datetime import datetime
>>> d = datetime(1899, 1, 1)
>>> d.strftime('%Y-%m-%d')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: year=1899 is before 1900; the datetime strftime() methods require year >= 1900
I'm sure I could hack together something myself to do this, but I figure the strftime function is there for a reason (and there also is a reason why it can't support pre-1900 dates). I need to be able to support dates before 1900. I'd just use str, but there's too much variation. In other words, it may or may not have microseconds or it may or may not have a timezone. Is there any solution to this?
If it makes a difference, I'm doing this so that I can write the data to a text file and load it into a database using Oracle SQL*Loader.
I essentially ended up doing Alex Martelli's answer. Here's a more complete implementation:
>>> from datetime import datetime
>>> d = datetime.now()
>>> d = d.replace(microsecond=0, tzinfo=None)
>>> str(d)
'2009-10-29 11:27:27'
The only difference is that str(d) is equivalent to d.isoformat(' ').
isoformat works on datetime instances w/o limitation of range:
>>> import datetime
>>> x=datetime.datetime(1865, 7, 2, 9, 30, 21)
>>> x.isoformat()
'1865-07-02T09:30:21'
If you need a different-format string it's not too hard to slice, dice and remix pieces of the string you get from isoformat, which is very consistent (YYYY-MM-DDTHH:MM:SS.mmmmmm, with the dot and following microseconds omitted if microseconds are zero).
The documentation seems pretty clear about this:
The exact range of years for which strftime() works also varies across platforms. Regardless of platform, years before 1900 cannot be used.
So there isn't going to be a solution that uses strftime(). Luckily, it's pretty straightforward to do this "by hand":
>>> "%02d-%02d-%02d %02d:%02d" % (d.year,d.month,d.day,d.hour,d.minute)
'1899-01-01 00:00'
mxDateTime can handle arbitrary dates. Python's time and datetime modules use UNIX timestamps internally, that's why they have limited range.
In [5]: mx.DateTime.DateTime(1899)
Out[5]: <mx.DateTime.DateTime object for '1899-01-01 00:00:00.00' at 154a960>
In [6]: DateTime.DateTime(1899).Format('%Y-%m-%d')
Out[6]: 1899-01-01
This is from the matplotlib source. Could provide a good starting point for rolling your own.
def strftime(self, dt, fmt):
fmt = self.illegal_s.sub(r"\1", fmt)
fmt = fmt.replace("%s", "s")
if dt.year > 1900:
return cbook.unicode_safe(dt.strftime(fmt))
year = dt.year
# For every non-leap year century, advance by
# 6 years to get into the 28-year repeat cycle
delta = 2000 - year
off = 6*(delta // 100 + delta // 400)
year = year + off
# Move to around the year 2000
year = year + ((2000 - year)//28)*28
timetuple = dt.timetuple()
s1 = time.strftime(fmt, (year,) + timetuple[1:])
sites1 = self._findall(s1, str(year))
s2 = time.strftime(fmt, (year+28,) + timetuple[1:])
sites2 = self._findall(s2, str(year+28))
sites = []
for site in sites1:
if site in sites2:
sites.append(site)
s = s1
syear = "%4d" % (dt.year,)
for site in sites:
s = s[:site] + syear + s[site+4:]
return cbook.unicode_safe(s)
This is the "feature" of the ctime library (UTF).
Also You may have problem above 2038.

Categories