72 votes

Comment puis-je produire une différence lisible par l'homme lors de la soustraction de deux timestamps UNIX en utilisant Python ?

Cette question est similaire à cette question sur la soustraction de dates avec Python mais pas identiques. Je n'ai pas affaire à des chaînes de caractères, je dois déterminer la différence entre deux horodatages d'époque et produire cette différence dans un format lisible par l'homme.

Par exemple :

32 Seconds
17 Minutes
22.3 Hours
1.25 Days
3.5 Weeks
2 Months
4.25 Years

Alternativement, j'aimerais exprimer la différence comme ceci :

4 years, 6 months, 3 weeks, 4 days, 6 hours 21 minutes and 15 seconds

Je ne pense pas que je puisse utiliser strptime puisque je travaille avec la différence de deux dates d'époque. Je pourrais écrire quelque chose pour faire cela, mais je suis sûr qu'il y a quelque chose de déjà écrit que je pourrais utiliser.

Quel module serait approprié ? Est-ce qu'il me manque quelque chose dans time ? Mon voyage dans le monde de Python ne fait que commencer. S'il s'agit bien d'un doublon, c'est parce que je n'ai pas su trouver ce qu'il fallait chercher.

Addendum

Pour ce qui est de la précision, c'est le calendrier de l'année en cours qui m'importe le plus.

0 votes

Par date d'époque UNIX, vous entendez le nombre habituel de secondes depuis X ?

1 votes

Quelle précision souhaitez-vous pour le calcul du mois et de l'année ? Cela peut devenir compliqué car le nombre de jours par mois et par an peut varier.

65voto

Schnouki Points 2872

Vous pouvez utiliser le merveilleux dateutil et son module relativedelta classe :

import datetime
import dateutil.relativedelta

dt1 = datetime.datetime.fromtimestamp(123456789) # 1973-11-29 22:33:09
dt2 = datetime.datetime.fromtimestamp(234567890) # 1977-06-07 23:44:50
rd = dateutil.relativedelta.relativedelta (dt2, dt1)

print "%d years, %d months, %d days, %d hours, %d minutes and %d seconds" % (rd.years, rd.months, rd.days, rd.hours, rd.minutes, rd.seconds)
# 3 years, 6 months, 9 days, 1 hours, 11 minutes and 41 seconds

Il ne compte pas les semaines, mais cela ne devrait pas être trop difficile à ajouter.

1 votes

Merci ! Cela fonctionne parfaitement. L'expression des semaines en jours ne pose pas de problème.

5 votes

Existe-t-il un moyen simple d'afficher uniquement les unités non nulles et de les mettre au pluriel automatiquement, par exemple "1 an, 2 minutes et 1 seconde" ?

0 votes

@PierredeLESPINAY Vous pouvez ternir les rd.second en tant qu'argument printf final et et de remplacer l'option s sur seconds avec %s c'est soit 's' soit '' selon la pluralité.

59voto

Sharoon Thomas Points 616

Une petite amélioration par rapport à la solution de @Schnouki avec une compréhension de la liste sur une seule ligne. Affiche également le pluriel dans le cas d'entités plurielles (comme les heures).

Importation relativedelta

>>> from dateutil.relativedelta import relativedelta

Une fonction lambda

>>> attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
>>> human_readable = lambda delta: ['%d %s' % (getattr(delta, attr), attr if getattr(delta, attr) > 1 else attr[:-1]) 
...     for attr in attrs if getattr(delta, attr)]

Exemple d'utilisation :

>>> human_readable(relativedelta(minutes=125))
['2 hours', '5 minutes']
>>> human_readable(relativedelta(hours=(24 * 365) + 1))
['365 days', '1 hour']

0 votes

a dû ajouter "microsecondes" à attrs pour des temps très courts pour éviter les erreurs, mais excellente méthode.

0 votes

Cela permet également d'éviter de créer des objets datetime à partir d'horodatages, alors qu'il suffit de stocker directement les dates d'époque. Ainsi, vous pouvez dire relativedelta(seconds=(t1 - t0)) .

25voto

Liudmil Mitev Points 318

J'ai eu exactement le même problème plus tôt dans la journée et je n'ai rien trouvé dans les bibliothèques standard que je pouvais utiliser, alors voici ce que j'ai écrit :

humaniser_temps.py

    #!/usr/bin/env python

    INTERVALS = [1, 60, 3600, 86400, 604800, 2419200, 29030400]
    NAMES = [('second', 'seconds'),
             ('minute', 'minutes'),
             ('hour', 'hours'),
             ('day', 'days'),
             ('week', 'weeks'),
             ('month', 'months'),
             ('year', 'years')]

    def humanize_time(amount, units):
    """
    Divide `amount` in time periods.
    Useful for making time intervals more human readable.

    >>> humanize_time(173, 'hours')
    [(1, 'week'), (5, 'hours')]
    >>> humanize_time(17313, 'seconds')
    [(4, 'hours'), (48, 'minutes'), (33, 'seconds')]
    >>> humanize_time(90, 'weeks')
    [(1, 'year'), (10, 'months'), (2, 'weeks')]
    >>> humanize_time(42, 'months')
    [(3, 'years'), (6, 'months')]
    >>> humanize_time(500, 'days')
    [(1, 'year'), (5, 'months'), (3, 'weeks'), (3, 'days')]
    """
       result = []

       unit = map(lambda a: a[1], NAMES).index(units)
       # Convert to seconds
       amount = amount * INTERVALS[unit]

       for i in range(len(NAMES)-1, -1, -1):
          a = amount / INTERVALS[i]
          if a > 0:
             result.append( (a, NAMES[i][1 % a]) )
             amount -= a * INTERVALS[i]

       return result

    if __name__ == "__main__":
        import doctest
        doctest.testmod()

Vous pouvez utiliser dateutil.relativedelta() pour calculer le delta horaire exact et l'humaniser avec ce script.

0 votes

Je suis curieux de savoir où vous obtenez vos chiffres pour les secondes en semaines, mois et années.

0 votes

Je l'ai écrit comme [1, 60, 60*60, 24*60*60, 7*24*60*60 ... ] à l'origine pour être plus évident mais j'ai pensé qu'il avait l'air long et ennuyeux, alors je l'ai changé en ceci. Le code actuel est simplement le résultat des multiplications.

0 votes

Comment pouvons-nous gérer amount des valeurs qui ont des décimales ? Par exemple : humanize_time(60.5, 'seconds') devrait donner 1 minute, 0.5 seconds

14voto

makiolo Points 176
def humanize_time(amount, units = 'seconds'):    

    def process_time(amount, units):

        INTERVALS = [   1, 60, 
                        60*60, 
                        60*60*24, 
                        60*60*24*7, 
                        60*60*24*7*4, 
                        60*60*24*7*4*12, 
                        60*60*24*7*4*12*100,
                        60*60*24*7*4*12*100*10]
        NAMES = [('second', 'seconds'),
                 ('minute', 'minutes'),
                 ('hour', 'hours'),
                 ('day', 'days'),
                 ('week', 'weeks'),
                 ('month', 'months'),
                 ('year', 'years'),
                 ('century', 'centuries'),
                 ('millennium', 'millennia')]

        result = []

        unit = map(lambda a: a[1], NAMES).index(units)
        # Convert to seconds
        amount = amount * INTERVALS[unit]

        for i in range(len(NAMES)-1, -1, -1):
            a = amount // INTERVALS[i]
            if a > 0: 
                result.append( (a, NAMES[i][1 % a]) )
                amount -= a * INTERVALS[i]

        return result

    rd = process_time(int(amount), units)
    cont = 0
    for u in rd:
        if u[0] > 0:
            cont += 1

    buf = ''
    i = 0
    for u in rd:
        if u[0] > 0:
            buf += "%d %s" % (u[0], u[1])
            cont -= 1

        if i < (len(rd)-1):
            if cont > 1:
                buf += ", "
            else:
                buf += " and "

        i += 1

    return buf

Exemple d'utilisation :

>>> print humanize_time(234567890 - 123456789)
3 years, 9 months, 3 weeks, 5 days, 11 minutes and 41 seconds
>>> humanize_time(9, 'weeks')
2 months and 1 week

Avantage (Vous n'avez pas besoin de tiers !).

Amélioré à partir de l'algorithme de "Liudmil Mitev". (Merci !)

0 votes

J'aime qu'aucun nouveau module ne doive être installé.

0 votes

Jusqu'à la semaine, c'est bien, mais dès qu'il commence à calculer le mois et l'année, il devient très imprécis. Selon la logique de l'intervalle, votre mois va être de 28 jours et l'année de 336 jours.

9voto

Riga Points 1029

Vieille question, mais personnellement je préfère cette approche :

import datetime
import math

def human_time(*args, **kwargs):
    secs  = float(datetime.timedelta(*args, **kwargs).total_seconds())
    units = [("day", 86400), ("hour", 3600), ("minute", 60), ("second", 1)]
    parts = []
    for unit, mul in units:
        if secs / mul >= 1 or mul == 1:
            if mul > 1:
                n = int(math.floor(secs / mul))
                secs -= n * mul
            else:
                n = secs if secs != int(secs) else int(secs)
            parts.append("%s %s%s" % (n, unit, "" if n == 1 else "s"))
    return ", ".join(parts)

human_time(seconds=3721)
# -> "1 hour, 2 minutes, 1 second"

Si vous voulez séparer les secondes par un "et", faites-le :

"%s and %s" % tuple(human_time(seconds=3721).rsplit(", ", 1))
# -> "1 hour, 2 minutes and 1 second"

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X