File size: 6,408 Bytes
7952f32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
"""Time humanizing functions."""

import datetime as dt
import math
from enum import Enum
from functools import total_ordering


@total_ordering
class Unit(Enum):
    MICROSECONDS = 0
    MILLISECONDS = 1
    SECONDS = 2
    MINUTES = 3
    HOURS = 4
    DAYS = 5
    MONTHS = 6
    YEARS = 7

    def __lt__(self, other):
        if self.__class__ is other.__class__:
            return self.value < other.value
        return NotImplemented


def _now():
    return dt.datetime.now()


def _abs_timedelta(delta):
    if delta.days < 0:
        now = _now()
        return now - (now + delta)
    return delta


def _date_and_delta(value, *, now=None):
    if not now:
        now = _now()
    if isinstance(value, dt.datetime):
        date = value
        delta = now - value
    elif isinstance(value, dt.timedelta):
        date = now - value
        delta = value
    else:
        try:
            value = int(value)
            delta = dt.timedelta(seconds=value)
            date = now - delta
        except (ValueError, TypeError):
            return None, value
    return date, _abs_timedelta(delta)


def naturaldelta(value, months=True, minimum_unit="seconds") -> str:
    """Return a natural representation of a timedelta or number of seconds.

    Does not include tense (use naturaltime for past/future).

    Examples:
        >>> import datetime as dt
        >>> naturaldelta(dt.timedelta(seconds=90))
        'a minute'
        >>> naturaldelta(dt.timedelta(hours=2))
        '2 hours'
        >>> naturaldelta(dt.timedelta(days=400))
        'a year'
    """
    tmp = Unit[minimum_unit.upper()]
    if tmp not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS):
        raise ValueError(f"Minimum unit '{minimum_unit}' not supported")
    minimum_unit = tmp

    if isinstance(value, dt.timedelta):
        delta = value
    else:
        try:
            value = int(value)
            delta = dt.timedelta(seconds=value)
        except (ValueError, TypeError):
            return value

    seconds = abs(delta.seconds)
    days = abs(delta.days)
    years = days // 365
    days = days % 365
    months_count = int(days // 30.5)

    if not years and days < 1:
        if seconds == 0:
            return "a moment"
        elif seconds == 1:
            return "a second"
        elif seconds < 60:
            return f"{seconds} seconds" if seconds > 1 else "a second"
        elif 60 <= seconds < 120:
            return "a minute"
        elif 120 <= seconds < 3600:
            minutes = seconds // 60
            return f"{minutes} minutes"
        elif 3600 <= seconds < 7200:
            return "an hour"
        else:
            hours = seconds // 3600
            return f"{hours} hours"
    elif years == 0:
        if days == 1:
            return "a day"
        if not months or not months_count:
            return f"{days} days"
        elif months_count == 1:
            return "a month"
        return f"{months_count} months"
    elif years == 1:
        if not months_count and not days:
            return "a year"
        elif not months_count:
            return f"1 year, {days} days" if days > 1 else "1 year, a day"
        elif months_count == 1:
            return "1 year, 1 month"
        return f"1 year, {months_count} months"
    return f"{years} years"


def naturaltime(value, future=False, months=True, minimum_unit="seconds", when=None) -> str:
    """Return a natural representation of a time relative to now.

    Examples:
        >>> import datetime as dt
        >>> naturaltime(dt.timedelta(seconds=30))
        '30 seconds ago'
        >>> naturaltime(dt.timedelta(hours=1), future=True)
        'an hour from now'
    """
    now = when or _now()
    date, delta = _date_and_delta(value, now=now)
    if date is None:
        return value
    if isinstance(value, (dt.datetime, dt.timedelta)):
        future = date > now
    ago = "%s from now" if future else "%s ago"
    delta_str = naturaldelta(delta, months, minimum_unit)
    if delta_str == "a moment":
        return "now"
    return ago % delta_str


def naturalday(value, format="%b %d") -> str:
    """Return 'today', 'tomorrow', 'yesterday', or a formatted date string.

    Examples:
        >>> import datetime as dt
        >>> naturalday(dt.date.today())
        'today'
    """
    try:
        value = dt.date(value.year, value.month, value.day)
    except (AttributeError, OverflowError, ValueError):
        return value
    delta = value - dt.date.today()
    if delta.days == 0:
        return "today"
    elif delta.days == 1:
        return "tomorrow"
    elif delta.days == -1:
        return "yesterday"
    return value.strftime(format)


def naturaldate(value) -> str:
    """Like naturalday, but appends year for dates more than ~5 months away."""
    try:
        value = dt.date(value.year, value.month, value.day)
    except (AttributeError, OverflowError, ValueError):
        return value
    delta = _abs_timedelta(value - dt.date.today())
    if delta.days >= 5 * 365 / 12:
        return naturalday(value, "%b %d %Y")
    return naturalday(value)


def precisedelta(value, minimum_unit="seconds", suppress=(), format="%0.2f") -> str:
    """Return a precise, human-readable representation of a timedelta.

    Examples:
        >>> import datetime as dt
        >>> precisedelta(dt.timedelta(seconds=3633, days=2))
        '2 days and 1 hour and 33 seconds'
    """
    date, delta = _date_and_delta(value)
    if date is None:
        return value

    suppress_units = {Unit[s.upper()] for s in suppress}
    min_unit = Unit[minimum_unit.upper()]

    days = delta.days
    secs = delta.seconds

    years, days = divmod(days, 365)
    months_count = int(days // 30.5)
    days = days % 30

    hours, secs = divmod(secs, 3600)
    minutes, secs = divmod(secs, 60)

    parts = []
    for count, singular, plural in [
        (years,         "year",   "years"),
        (months_count,  "month",  "months"),
        (days,          "day",    "days"),
        (hours,         "hour",   "hours"),
        (minutes,       "minute", "minutes"),
        (secs,          "second", "seconds"),
    ]:
        if count > 0:
            label = singular if count == 1 else plural
            parts.append(f"{count} {label}")

    if not parts:
        return "0 seconds"
    if len(parts) == 1:
        return parts[0]
    return " and ".join(parts)