92

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?

martineau
  • 119,623
  • 25
  • 170
  • 301
Parham
  • 3,157
  • 4
  • 31
  • 44

12 Answers12

123

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.

steveha
  • 74,789
  • 21
  • 92
  • 117
  • Your rounding method doesn't work for me, it produces this result: `'2015-04-12 17:09:020.588'`, the seconds are invalid. – NuclearPeon Apr 13 '15 at 00:12
  • 2
    I tested your code in python 2.7.6 and python 3.4.0. Copied and pasted your code, same result each time. You have 3 digits in your seconds area. I'm looking at the code and it seems your float (`f` variable) tacks on the leading 0 before the decimal (ex: `0.367`) which is appended as a string. That leading 0 in the seconds area is where it starts failing. – NuclearPeon Apr 15 '15 at 03:41
  • 3
    @NuclearPeon I apologize. You identified a real bug, which proves I didn't actually test this before posting it. I have edited the code to fix the bug and it should work for you now. Thank you for bringing this to my attention. – steveha Apr 15 '15 at 15:57
  • This does not work. When the microsecond value happens to be zero, the seconds value gets truncated e.g: '2007-12-24T11:32'. – Mohamed May 20 '16 at 20:23
  • When microseconds is at least 999500, this truncates instead of rounding up. You can fix this after defining `f`: `if f >= 1.0: return (t + datetime.timedelta(milliseconds=1)).strftime('%Y-%m-%d %H:%M:%S.000')` (and otherwise continue as before). – Nick Matteo Apr 13 '17 at 17:23
  • This doesn't work for `datefmt` in `logging.basicConfig` – Att Righ Dec 02 '20 at 12:09
51

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
Rob
  • 715
  • 5
  • 9
11

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.

Eric Herot
  • 388
  • 3
  • 6
  • 1
    Thanks, this works best for me. The only thing I updated is hardcoded `"Z"` into main string template instead of using `"%z"`. – wowkin2 Dec 26 '19 at 14:12
  • @Eric Herot : I appreciate this answer. '2020-05-31T14:07:3:172Z' instead of '2020-05-31T14:07:03.172Z' , can it be handled to add leading zero to the minute ? – StackGuru May 31 '20 at 08:40
  • @Eric Herot : I got it, return "%s:%06.3f%s" % ( dt.strftime('%Y-%m-%dT%H:%M'), float("%06.3f" % (dt.second + dt.microsecond / 1e6)), dt.strftime('Z') ) – StackGuru May 31 '20 at 08:50
  • @StackGuru Weird that you would be getting a non-zero padded minute with that formatting string. According to the docs (https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) it should work that way by default. – Eric Herot Jun 02 '20 at 14:13
  • Like several other solutions, this doesn't round properly: time_format(datetime.datetime(2020, 1, 1, 23, 59, 59, 999999)) returns '2020-01-01T23:59:60.000' – jtniehof Aug 30 '21 at 19:45
5

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.

Abdurrahman
  • 51
  • 1
  • 1
4

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.
Apteryx
  • 5,822
  • 3
  • 16
  • 18
  • microsecond is 0 <= microseconds < 1000000. So exactly for million it will produce "1000". This is exactly what happened to me https://www.devrant.io/rants/466105 – Dawid Gosławski Mar 08 '17 at 12:13
  • 1
    @alkuzad: As you mention, it cannot be exactly a million, but `format` will round it to 1000 when microsecond is at least 999500. Instead of rounding, you can add 500 microseconds to the datetime and truncate to get the same effect, while automatically taking care of the round-up rolling over to minutes, hours, days, months, or years. Or just truncate, if being half a millisecond off doesn't matter much to you: `'{:03}'.format(datetime.now().microsecond // 1000)` – Nick Matteo Apr 13 '17 at 17:29
4

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

user
  • 1,022
  • 2
  • 8
  • 30
2

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())
iurii
  • 2,597
  • 22
  • 25
2

steveha already said truncating. That will round you down. But if you want to round to the nearest digit, the way to round numbers in general is to add 5 to the place of decimals you would be cutting off, before doing the truncating. So for example to round 12.3456 to 3 places of decimals, add 0.0005; that gives you 12.3461 which you then truncate to 3 characters after the point, giving you 12.346.

The question is, how to add 5 to the next place of decimals -- correctly. So you can't just consider the microseconds, if you round up .9995 seconds when it's just before midnight on New Year's eve, the carry might go all the way up to the year!

But you can use a timedelta. Then the time calculation is correct and you can just truncate to get your correctly-rounded value.

To round to 3 places of decimals in seconds, you want to add 0.0005 seconds = 500 microseconds. Here are 3 examples: first, round 0.1234 seconds to 0.123, then round 0.1235 seconds to 0.124, then have the carry propagate all the way when you should be kissing someone to wish them happy new year and instead you're doing Python:

>>> r = datetime.timedelta(microseconds=500)
>>> t = datetime.datetime(2023,1,2,3,45,6,123456)
>>> (t+r).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
'2023-01-02 03:45:06.123'
>>> t = datetime.datetime(2023,1,2,3,45,6,123556)
>>> (t+r).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
'2023-01-02 03:45:06.124'
>>> t = datetime.datetime(2023,12,31,23,59,59,999500)
>>> (t+r).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
'2024-01-01 00:00:00.000'
fpeelo
  • 372
  • 2
  • 10
1

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.

NuclearPeon
  • 5,743
  • 4
  • 44
  • 52
0

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)
gandreoti
  • 15
  • 6
  • this fails for 59.5->59.999' seconds as it then tries to set the seconds field to 60. – Pablojim Apr 06 '18 at 09:33
  • 1
    @Pablojim fixed and removed log10 as function, no need it at all. it was really rounding everything to just seconds. Thanks – gandreoti Apr 14 '18 at 02:59
0

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,

Jai K
  • 375
  • 1
  • 4
  • 12
0

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'