Python function to convert seconds into minutes, hours, and days

前端 未结 16 1327
不思量自难忘°
不思量自难忘° 2020-11-28 07:27

Question: Write a program that asks the user to enter a number of seconds, and works as follows:

  • There are 60 seconds in a minute. If the number of seconds

相关标签:
16条回答
  • 2020-11-28 08:14

    These functions are fairly compact and only use standard Python 2.6 and later.

    def ddhhmmss(seconds):
        """Convert seconds to a time string "[[[DD:]HH:]MM:]SS".
        """
        dhms = ''
        for scale in 86400, 3600, 60:
            result, seconds = divmod(seconds, scale)
            if dhms != '' or result > 0:
                dhms += '{0:02d}:'.format(result)
        dhms += '{0:02d}'.format(seconds)
        return dhms
    
    
    def seconds(dhms):
        """Convert a time string "[[[DD:]HH:]MM:]SS" to seconds.
        """
        components = [int(i) for i in dhms.split(':')]
        pad = 4 - len(components)
        if pad < 0:
            raise ValueError('Too many components to match [[[DD:]HH:]MM:]SS')
        components = [0] * pad + components
        return sum(i * j for i, j in zip((86400, 3600, 60, 1), components))
    

    And here are tests to go with them. I'm using the pytest package as a simple way to test exceptions.

    import ddhhmmss
    
    import pytest
    
    
    def test_ddhhmmss():
        assert ddhhmmss.ddhhmmss(0) == '00'
        assert ddhhmmss.ddhhmmss(2) == '02'
        assert ddhhmmss.ddhhmmss(12 * 60) == '12:00'
        assert ddhhmmss.ddhhmmss(3600) == '01:00:00'
        assert ddhhmmss.ddhhmmss(10 * 86400) == '10:00:00:00'
        assert ddhhmmss.ddhhmmss(86400 + 5 * 3600 + 30 * 60 + 1) == '01:05:30:01'
        assert ddhhmmss.ddhhmmss(365 * 86400) == '365:00:00:00'
    
    
    def test_seconds():
        assert ddhhmmss.seconds('00') == 0
        assert ddhhmmss.seconds('02') == 2
        assert ddhhmmss.seconds('12:00') == 12 * 60
        assert ddhhmmss.seconds('01:00:00') == 3600
        assert ddhhmmss.seconds('1:0:0') == 3600
        assert ddhhmmss.seconds('3600') == 3600
        assert ddhhmmss.seconds('60:0') == 3600
        assert ddhhmmss.seconds('10:00:00:00') == 10 * 86400
        assert ddhhmmss.seconds('1:05:30:01') == 86400 + 5 * 3600 + 30 * 60 + 1
        assert ddhhmmss.seconds('365:00:00:00') == 365 * 86400
    
    
    def test_seconds_raises():
        with pytest.raises(ValueError):
            ddhhmmss.seconds('')
        with pytest.raises(ValueError):
            ddhhmmss.seconds('foo')
        with pytest.raises(ValueError):
            ddhhmmss.seconds('1:00:00:00:00')
    
    0 讨论(0)
  • 2020-11-28 08:17

    Patching as well Ralph Bolton's answer. Moving to a class and moving tulp of tulp (intervals) to dictionary. Adding an optional rounded function depending of granularity (enable by default). Ready to translation using gettext (default is disable). This is intend to be load from an module. This is for python3 (tested 3.6 - 3.8)

    import gettext
    import locale
    from itertools import chain
    
    mylocale = locale.getdefaultlocale()
    # see --> https://stackoverflow.com/a/10174657/11869956 thx 
    #localedir = os.path.join(os.path.dirname(__file__), 'locales')
    # or python > 3.4:
    try:
        localedir = pathlib.Path(__file__).parent/'locales'
        lang_translations = gettext.translation('utils', localedir, 
                                                languages=[mylocale[0]])
        lang_translations.install()
        _ = lang_translations.gettext
    except Exception as exc:
        print('Error: unexcept error while initializing translation:', file=sys.stderr)
        print(f'Error: {exc}', file=sys.stderr)
        print(f'Error: localedir={localedir}, languages={mylocale[0]}', file=sys.stderr)
        print('Error: translation has been disabled.', file=sys.stderr)
        _ = gettext.gettext
    

    Here is the class:

    class FormatTimestamp:
        """Convert seconds to, optional rounded, time depending of granularity's degrees.
            inspired by https://stackoverflow.com/a/24542445/11869956"""
        def __init__(self):
            # For now i haven't found a way to do it better
            # TODO: optimize ?!? ;)
            self.intervals = {
                # 'years'     :   31556952,  # https://www.calculateme.com/time/years/to-seconds/
                # https://www.calculateme.com/time/months/to-seconds/ -> 2629746 seconds
                # But it's outputing some strange result :
                # So 3 seconds less (2629743) : 4 weeks, 2 days, 10 hours, 29 minutes and 3 seconds
                # than after 3 more seconds : 1 month ?!?
                # Google give me 2628000 seconds
                # So 3 seconds less (2627997): 4 weeks, 2 days, 9 hours, 59 minutes and 57 seconds
                # Strange as well 
                # So for the moment latest is week ...
                #'months'    :   2419200, # 60 * 60 * 24 * 7 * 4 
                'weeks'     :   604800,  # 60 * 60 * 24 * 7
                'days'      :   86400,    # 60 * 60 * 24
                'hours'     :   3600,    # 60 * 60
                'minutes'   :   60,
                'seconds'  :   1
                }
            self.nextkey = {
                'seconds'   :   'minutes',
                'minutes'   :   'hours',
                'hours'     :   'days',
                'days'      :   'weeks',
                'weeks'     :   'weeks',
                #'months'    :   'months',
                #'years'     :   'years' # stop here
                }
            self.translate = {
                'weeks'     :   _('weeks'),
                'days'      :   _('days'),
                'hours'     :   _('hours'),
                'minutes'   :   _('minutes'),
                'seconds'   :   _('seconds'),
                ## Single
                'week'      :   _('week'),
                'day'       :   _('day'),
                'hour'      :   _('hour'),
                'minute'    :   _('minute'),
                'second'    :   _('second'),
                ' and'      :   _('and'),
                ','         :   _(','),     # This is for compatibility
                ''          :   '\0'        # same here BUT we CANNOT pass empty string to gettext 
                                            # or we get : warning: Empty msgid.  It is reserved by GNU gettext:
                                            # gettext("") returns the header entry with
                                            # meta information, not the empty string.
                                            # Thx to --> https://stackoverflow.com/a/30852705/11869956 - saved my day
                }
    
        def convert(self, seconds, granularity=2, rounded=True, translate=False):
            """Proceed the conversion"""
    
            def _format(result):
                """Return the formatted result
                TODO : numpy / google docstrings"""
                start = 1 
                length = len(result)
                none = 0
                next_item = False
                for item in reversed(result[:]):
                    if item['value']:
                        # if we have more than one item
                        if length - none > 1:
                            # This is the first 'real' item 
                            if start == 1:
                                item['punctuation'] = ''
                                next_item = True
                            elif next_item:
                                # This is the second 'real' item
                                # Happened 'and' to key name
                                item['punctuation'] = ' and'
                                next_item = False
                            # If there is more than two 'real' item
                            # than happened ','
                            elif 2 < start:
                                item['punctuation'] = ','
                            else:
                                item['punctuation'] = ''
                        else:
                            item['punctuation'] = ''
                        start += 1
                    else:
                        none += 1
                return [ { 'value'        :   mydict['value'], 
                           'name'         :   mydict['name_strip'],
                           'punctuation'  :   mydict['punctuation'] } for mydict in result \
                                                                      if mydict['value'] is not None ]
    
    
            def _rstrip(value, name):
                """Rstrip 's' name depending of value"""
                if value == 1:
                    name = name.rstrip('s')
                return name
    
    
            # Make sure granularity is an integer
            if not isinstance(granularity, int):
                raise ValueError(f'Granularity should be an integer: {granularity}')
    
            # For seconds only don't need to compute
            if seconds < 0:
                return 'any time now.'
            elif seconds < 60:
                return 'less than a minute.'
    
            result = []
            for name, count in self.intervals.items():
                value = seconds // count
                if value:
                    seconds -= value * count
                    name_strip = _rstrip(value, name)
                    # save as dict: value, name_strip (eventually strip), name (for reference), value in seconds
                    # and count (for reference)
                    result.append({ 
                            'value'        :   value,
                            'name_strip'   :   name_strip,
                            'name'         :   name, 
                            'seconds'      :   value * count,
                            'count'        :   count
                                     })
                else:
                    if len(result) > 0:
                        # We strip the name as second == 0
                        name_strip = name.rstrip('s')
                        # adding None to key 'value' but keep other value
                        # in case when need to add seconds when we will 
                        # recompute every thing
                        result.append({ 
                            'value'        :   None,
                            'name_strip'   :   name_strip,
                            'name'         :   name, 
                            'seconds'      :   0,
                            'count'        :   count
                                     })
    
            # Get the length of the list
            length = len(result)
            # Don't need to compute everything / every time
            if length < granularity or not rounded:
                if translate:
                    return ' '.join('{0} {1}{2}'.format(item['value'], _(self.translate[item['name']]), 
                                                    _(self.translate[item['punctuation']])) \
                                                    for item in _format(result))
                else:
                    return ' '.join('{0} {1}{2}'.format(item['value'], item['name'], item['punctuation']) \
                                                    for item in _format(result))
    
            start = length - 1
            # Reverse list so the firsts elements 
            # could be not selected depending on granularity.
            # And we can delete item after we had his seconds to next
            # item in the current list (result)
            for item in reversed(result[:]):
                if granularity <= start <= length - 1:
                    # So we have to round
                    current_index = result.index(item)
                    next_index = current_index - 1
                    # skip item value == None
                    # if the seconds of current item is superior
                    # to the half seconds of the next item: round
                    if item['value'] and item['seconds'] > result[next_index]['count'] // 2:
                        # +1 to the next item (in seconds: depending on item count)
                        result[next_index]['seconds'] += result[next_index]['count']
                    # Remove item which is not selected
                    del result[current_index]
                start -= 1
            # Ok now recalculate everything
            # Reverse as well 
            for item in reversed(result[:]):
                # Check if seconds is superior or equal to the next item 
                # but not from 'result' list but from 'self.intervals' dict
                # Make sure it's not None
                if item['value']:
                    next_item_name = self.nextkey[item['name']]
                    # This mean we are at weeks
                    if item['name'] == next_item_name:
                        # Just recalcul
                        item['value'] = item['seconds'] // item['count']
                        item['name_strip'] = _rstrip(item['value'], item['name'])
                    # Stop to weeks to stay 'right' 
                    elif item['seconds'] >= self.intervals[next_item_name]:
                        # First make sure we have the 'next item'
                        # found via --> https://stackoverflow.com/q/26447309/11869956
                        # maybe there is a faster way to do it ? - TODO
                        if any(search_item['name'] == next_item_name for search_item in result):
                            next_item_index = result.index(item) - 1
                            # Append to
                            result[next_item_index]['seconds'] += item['seconds']
                            # recalculate value
                            result[next_item_index]['value'] = result[next_item_index]['seconds'] // \
                                                               result[next_item_index]['count']
                            # strip or not
                            result[next_item_index]['name_strip'] = _rstrip(result[next_item_index]['value'],
                                                                           result[next_item_index]['name'])
                        else:
                            # Creating 
                            next_item_index = result.index(item) - 1
                            # get count
                            next_item_count = self.intervals[next_item_name]
                            # convert seconds
                            next_item_value = item['seconds'] // next_item_count
                            # strip 's' or not
                            next_item_name_strip = _rstrip(next_item_value, next_item_name)
                            # added to dict
                            next_item = {
                                           'value'      :   next_item_value,
                                           'name_strip' :   next_item_name_strip,
                                           'name'       :   next_item_name,
                                           'seconds'    :   item['seconds'],
                                           'count'      :   next_item_count
                                           }
                            # insert to the list
                            result.insert(next_item_index, next_item)
                        # Remove current item
                        del result[result.index(item)]
                    else:
                        # for current item recalculate
                        # keys 'value' and 'name_strip'
                        item['value'] = item['seconds'] // item['count']
                        item['name_strip'] = _rstrip(item['value'], item['name'])
            if translate:
                return ' '.join('{0} {1}{2}'.format(item['value'], 
                                                    _(self.translate[item['name']]), 
                                                    _(self.translate[item['punctuation']])) \
                                                    for item in _format(result))
            else:
                return ' '.join('{0} {1}{2}'.format(item['value'], item['name'], item['punctuation']) \
                                                    for item in _format(result))
    

    To use it:

    myformater = FormatTimestamp()
    myconverter = myformater.convert(seconds) 
    

    granularity = 1 - 5, rounded = True / False, translate = True / False

    Some test to show difference:

    myformater = FormatTimestamp()
    for firstrange in [131440, 563440, 604780, 2419180, 113478160]:
        print(f'#### Seconds : {firstrange} ####')
        print('\tFull - function: {0}'.format(display_time(firstrange, granularity=5)))
        print('\tFull -    class: {0}'.format(myformater.convert(firstrange, granularity=5))) 
        for secondrange in range(1, 6, 1):
            print('\tGranularity   this   answer ({0}): {1}'.format(secondrange, 
                                                                 myformater.convert(firstrange,
                                                                                    granularity=secondrange, translate=False)))
            print('\tGranularity Bolton\'s answer ({0}): {1}'.format(secondrange, display_time(firstrange,
                                                                                    granularity=secondrange)))
        print()
    
    Seconds : 131440
        Full - function: 1 day, 12 hours, 30 minutes, 40 seconds
        Full -    class: 1 day, 12 hours, 30 minutes and 40 seconds
        Granularity   this   answer (1): 2 days
        Granularity Bolton's answer (1): 1 day
        Granularity   this   answer (2): 1 day and 13 hours
        Granularity Bolton's answer (2): 1 day, 12 hours
        Granularity   this   answer (3): 1 day, 12 hours and 31 minutes
        Granularity Bolton's answer (3): 1 day, 12 hours, 30 minutes
        Granularity   this   answer (4): 1 day, 12 hours, 30 minutes and 40 seconds
        Granularity Bolton's answer (4): 1 day, 12 hours, 30 minutes, 40 seconds
        Granularity   this   answer (5): 1 day, 12 hours, 30 minutes and 40 seconds
        Granularity Bolton's answer (5): 1 day, 12 hours, 30 minutes, 40 seconds
    
    Seconds : 563440
        Full - function: 6 days, 12 hours, 30 minutes, 40 seconds
        Full -    class: 6 days, 12 hours, 30 minutes and 40 seconds
        Granularity   this   answer (1): 1 week
        Granularity Bolton's answer (1): 6 days
        Granularity   this   answer (2): 6 days and 13 hours
        Granularity Bolton's answer (2): 6 days, 12 hours
        Granularity   this   answer (3): 6 days, 12 hours and 31 minutes
        Granularity Bolton's answer (3): 6 days, 12 hours, 30 minutes
        Granularity   this   answer (4): 6 days, 12 hours, 30 minutes and 40 seconds
        Granularity Bolton's answer (4): 6 days, 12 hours, 30 minutes, 40 seconds
        Granularity   this   answer (5): 6 days, 12 hours, 30 minutes and 40 seconds
        Granularity Bolton's answer (5): 6 days, 12 hours, 30 minutes, 40 seconds
    
    Seconds : 604780
        Full - function: 6 days, 23 hours, 59 minutes, 40 seconds
        Full -    class: 6 days, 23 hours, 59 minutes and 40 seconds
        Granularity   this   answer (1): 1 week
        Granularity Bolton's answer (1): 6 days
        Granularity   this   answer (2): 1 week
        Granularity Bolton's answer (2): 6 days, 23 hours
        Granularity   this   answer (3): 1 week
        Granularity Bolton's answer (3): 6 days, 23 hours, 59 minutes
        Granularity   this   answer (4): 6 days, 23 hours, 59 minutes and 40 seconds
        Granularity Bolton's answer (4): 6 days, 23 hours, 59 minutes, 40 seconds
        Granularity   this   answer (5): 6 days, 23 hours, 59 minutes and 40 seconds
        Granularity Bolton's answer (5): 6 days, 23 hours, 59 minutes, 40 seconds
    
    Seconds : 2419180
        Full - function: 3 weeks, 6 days, 23 hours, 59 minutes, 40 seconds
        Full -    class: 3 weeks, 6 days, 23 hours, 59 minutes and 40 seconds
        Granularity   this   answer (1): 4 weeks
        Granularity Bolton's answer (1): 3 weeks
        Granularity   this   answer (2): 4 weeks
        Granularity Bolton's answer (2): 3 weeks, 6 days
        Granularity   this   answer (3): 4 weeks
        Granularity Bolton's answer (3): 3 weeks, 6 days, 23 hours
        Granularity   this   answer (4): 4 weeks
        Granularity Bolton's answer (4): 3 weeks, 6 days, 23 hours, 59 minutes
        Granularity   this   answer (5): 3 weeks, 6 days, 23 hours, 59 minutes and 40 seconds
        Granularity Bolton's answer (5): 3 weeks, 6 days, 23 hours, 59 minutes, 40 seconds
    
    Seconds : 113478160
        Full - function: 187 weeks, 4 days, 9 hours, 42 minutes, 40 seconds
        Full -    class: 187 weeks, 4 days, 9 hours, 42 minutes and 40 seconds
        Granularity   this   answer (1): 188 weeks
        Granularity Bolton's answer (1): 187 weeks
        Granularity   this   answer (2): 187 weeks and 4 days
        Granularity Bolton's answer (2): 187 weeks, 4 days
        Granularity   this   answer (3): 187 weeks, 4 days and 10 hours
        Granularity Bolton's answer (3): 187 weeks, 4 days, 9 hours
        Granularity   this   answer (4): 187 weeks, 4 days, 9 hours and 43 minutes
        Granularity Bolton's answer (4): 187 weeks, 4 days, 9 hours, 42 minutes
        Granularity   this   answer (5): 187 weeks, 4 days, 9 hours, 42 minutes and 40 seconds
        Granularity Bolton's answer (5): 187 weeks, 4 days, 9 hours, 42 minutes, 40 seconds
    

    I have a french translation ready. But it's fast to do the translation ... just few words. Hope this could help as the other answer help me a lot.

    0 讨论(0)
  • 2020-11-28 08:18

    Patching Mr.B's answer (sorry, not enough rep. to comment), we can return variable granularity based on the amount of time. For example, we don't say "1 week, 5 seconds", we just say "1 week":

    def display_time(seconds, granularity=2):
        result = []
    
        for name, count in intervals:
            value = seconds // count
            if value:
                seconds -= value * count
                if value == 1:
                    name = name.rstrip('s')
                result.append("{} {}".format(value, name))
            else:
                # Add a blank if we're in the middle of other values
                if len(result) > 0:
                    result.append(None)
        return ', '.join([x for x in result[:granularity] if x is not None])
    

    Some sample input:

    for diff in [5, 67, 3600, 3605, 3667, 24*60*60, 24*60*60+5, 24*60*60+57, 24*60*60+3600, 24*60*60+3667, 2*24*60*60, 2*24*60*60+5*60*60, 7*24*60*60, 7*24*60*60 + 24*60*60]:
        print "For %d seconds: %s" % (diff, display_time(diff, 2))
    

    ...returns this output:

    For 5 seconds: 5 seconds
    For 67 seconds: 1 minute, 7 seconds
    For 3600 seconds: 1 hour
    For 3605 seconds: 1 hour
    For 3667 seconds: 1 hour, 1 minute
    For 86400 seconds: 1 day
    For 86405 seconds: 1 day
    For 86457 seconds: 1 day
    For 90000 seconds: 1 day, 1 hour
    For 90067 seconds: 1 day, 1 hour
    For 172800 seconds: 2 days
    For 190800 seconds: 2 days, 5 hours
    For 604800 seconds: 1 week
    For 691200 seconds: 1 week, 1 day
    
    0 讨论(0)
  • 2020-11-28 08:19

    At first glance, I figured divmod would be faster since it's a single statement and a built-in function, but timeit seems to show otherwise. Consider this little example I came up with when I was trying to figure out the fastest method for use in a loop that continuously runs in a gobject idle_add splitting a seconds counter into a human readable time for updating a progress bar label.

    import timeit
    
    def test1(x,y, dropy):
        while x > 0:
            y -= dropy
            x -= 1
    
            # the test
            minutes = (y-x) / 60
            seconds = (y-x) % 60.0
    
    def test2(x,y, dropy):
        while x > 0:
            y -= dropy
            x -= 1
    
            # the test
            minutes, seconds = divmod((y-x), 60)
    
    x = 55     # litte number, also number of tests
    y = 10000  # make y > x by factor of drop
    dropy = 7 # y is reduced this much each iteration, for variation
    
    print "division and modulus:", timeit.timeit( lambda: test1(x,y,dropy) )
    print "divmod function:",      timeit.timeit( lambda: test2(x,y,dropy) )
    

    The built-in divmod function seems incredibly slower compared to using the simple division and modulus.

    division and modulus: 12.5737669468
    divmod function: 17.2861430645
    
    0 讨论(0)
提交回复
热议问题