Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
time.py673 linesDownload Raw Back to humanize
1"""Time humanizing functions.2 3These are largely borrowed from Django's `contrib.humanize`.4"""5 6from __future__ import annotations7 8from enum import Enum9from functools import total_ordering10 11from .i18n import _gettext as _12from .i18n import _ngettext13from .number import intcomma14 15TYPE_CHECKING = False16if TYPE_CHECKING:17    import datetime as dt18    from collections.abc import Iterable19    from typing import Any20 21__all__ = [22    "naturaldate",23    "naturalday",24    "naturaldelta",25    "naturaltime",26    "precisedelta",27]28 29 30@total_ordering31class Unit(Enum):32    MICROSECONDS = 033    MILLISECONDS = 134    SECONDS = 235    MINUTES = 336    HOURS = 437    DAYS = 538    MONTHS = 639    YEARS = 740 41    def __lt__(self, other: Any) -> Any:42        if self.__class__ is other.__class__:43            return self.value < other.value44        return NotImplemented45 46 47def _now() -> dt.datetime:48    import datetime as dt49 50    return dt.datetime.now()51 52 53def _abs_timedelta(delta: dt.timedelta) -> dt.timedelta:54    """Return an "absolute" value for a timedelta, always representing a time distance.55 56    Args:57        delta (datetime.timedelta): Input timedelta.58 59    Returns:60        datetime.timedelta: Absolute timedelta.61    """62    if delta.days < 0:63        now = _now()64        return now - (now + delta)65    return delta66 67 68def _date_and_delta(69    value: Any, *, now: dt.datetime | None = None, precise: bool = False70) -> tuple[Any, Any]:71    """Turn a value into a date and a timedelta which represents how long ago it was.72 73    If that's not possible, return `(None, value)`.74    """75    import datetime as dt76 77    if not now:78        now = _now()79    if isinstance(value, dt.datetime):80        date = value81        delta = now - value82    elif isinstance(value, dt.timedelta):83        date = now - value84        delta = value85    else:86        try:87            value = value if precise else round(value)88            delta = dt.timedelta(seconds=value)89            date = now - delta90        except (ValueError, TypeError):91            return None, value92    return date, _abs_timedelta(delta)93 94 95def naturaldelta(96    value: dt.timedelta | float,97    months: bool = True,98    minimum_unit: str = "seconds",99) -> str:100    """Return a natural representation of a timedelta or number of seconds.101 102    This is similar to `naturaltime`, but does not add tense to the result.103 104    The timedelta will be rounded to the nearest unit that makes sense.105 106    Args:107        value (datetime.timedelta, int or float): A timedelta or a number of seconds.108        months (bool): If `True`, then a number of months (based on 30.5 days) will be109            used for fuzziness between years.110        minimum_unit (str): The lowest unit that can be used.111 112    Returns:113        str (str or `value`): A natural representation of the amount of time114            elapsed unless `value` is not datetime.timedelta or cannot be115            converted to int (cannot be float due to 'inf' or 'nan').116            In that case, a `value` is returned unchanged.117 118    Raises:119        OverflowError: If `value` is too large to convert to datetime.timedelta.120 121    Examples:122        Compare two timestamps in a custom local timezone::123 124        ```pycon125        >>> import datetime as dt126        >>> from dateutil.tz import gettz127 128        >>> berlin = gettz("Europe/Berlin")129        >>> now = dt.datetime.now(tz=berlin)130        >>> later = now + dt.timedelta(minutes=30)131 132        >>> assert naturaldelta(later - now) == "30 minutes"133        True134        ```135 136    """137    import datetime as dt138 139    tmp = Unit[minimum_unit.upper()]140    if tmp not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS):141        msg = f"Minimum unit '{minimum_unit}' not supported"142        raise ValueError(msg)143    min_unit = tmp144 145    if isinstance(value, dt.timedelta):146        delta = value147    else:148        try:149            int(value)  # Explicitly don't support string such as "NaN" or "inf"150            value = float(value)151            delta = dt.timedelta(seconds=value)152        except (ValueError, TypeError):153            return str(value)154 155    use_months = months156 157    delta = abs(delta)158    years = delta.days // 365159    days = delta.days % 365160    num_months = round(days / 30.5)161 162    if years == 0 and days < 1:163        if delta.seconds == 0:164            if min_unit == Unit.MICROSECONDS and delta.microseconds < 1000:165                return (166                    _ngettext("%d microsecond", "%d microseconds", delta.microseconds)167                    % delta.microseconds168                )169 170            if min_unit == Unit.MILLISECONDS or (171                min_unit == Unit.MICROSECONDS and 1000 <= delta.microseconds < 1_000_000172            ):173                milliseconds = delta.microseconds / 1000174                return (175                    _ngettext("%d millisecond", "%d milliseconds", int(milliseconds))176                    % milliseconds177                )178            return _("a moment")179 180        if delta.seconds == 1:181            return _("a second")182 183        if delta.seconds < 60:184            return _ngettext("%d second", "%d seconds", delta.seconds) % delta.seconds185 186        if 60 <= delta.seconds < 3600:187            minutes = round(delta.seconds / 60)188            if minutes == 1:189                return _("a minute")190 191            if minutes == 60:192                return _("an hour")193 194            return _ngettext("%d minute", "%d minutes", minutes) % minutes195 196        if 3600 <= delta.seconds:197            hours = round(delta.seconds / 3600)198            if hours == 1:199                return _("an hour")200 201            if hours == 24:202                return _("a day")203 204            return _ngettext("%d hour", "%d hours", hours) % hours205 206    elif years == 0:207        if days == 1:208            return _("a day")209 210        if not use_months:211            return _ngettext("%d day", "%d days", days) % days212 213        if num_months == 0:214            return _ngettext("%d day", "%d days", days) % days215 216        if num_months == 1:217            return _("a month")218 219        if num_months == 12:220            return _("a year")221 222        return _ngettext("%d month", "%d months", num_months) % num_months223 224    elif years == 1:225        if num_months == 0 and days == 0:226            return _("a year")227 228        if num_months == 0:229            return _ngettext("1 year, %d day", "1 year, %d days", days) % days230 231        if use_months:232            if num_months == 1:233                return _("1 year, 1 month")234 235            if num_months == 12:236                years += 1237                return _ngettext("%d year", "%d years", years) % years238 239            return (240                _ngettext("1 year, %d month", "1 year, %d months", num_months)241                % num_months242            )243 244        return _ngettext("1 year, %d day", "1 year, %d days", days) % days245 246    return _ngettext("%d year", "%d years", years).replace("%d", "%s") % intcomma(years)247 248 249def naturaltime(250    value: dt.datetime | dt.timedelta | float,251    future: bool = False,252    months: bool = True,253    minimum_unit: str = "seconds",254    when: dt.datetime | None = None,255) -> str:256    """Return a natural representation of a time in a resolution that makes sense.257 258    This is more or less compatible with Django's `naturaltime` filter.259 260    The time will be rounded to the nearest unit that makes sense.261 262    Args:263        value (datetime.datetime, datetime.timedelta, int or float): A `datetime`, a264            `timedelta`, or a number of seconds.265        future (bool): Ignored for `datetime`s and `timedelta`s, where the tense is266            always figured out based on the current time. For integers and floats, the267            return value will be past tense by default, unless future is `True`.268        months (bool): If `True`, then a number of months (based on 30.5 days) will be269            used for fuzziness between years.270        minimum_unit (str): The lowest unit that can be used.271        when (datetime.datetime): Point in time relative to which _value_ is272            interpreted.  Defaults to the current time in the local timezone.273 274    Returns:275        str: A natural representation of the input in a resolution that makes sense.276    """277    import datetime as dt278 279    value = _convert_aware_datetime(value)280    when = _convert_aware_datetime(when)281 282    now = when or _now()283 284    date, delta = _date_and_delta(value, now=now)285    if date is None:286        return str(value)287    # determine tense by value only if datetime/timedelta were passed288    if isinstance(value, (dt.datetime, dt.timedelta)):289        future = date > now290 291    ago = _("%s from now") if future else _("%s ago")292    delta = naturaldelta(delta, months, minimum_unit)293 294    if delta == _("a moment"):295        return _("now")296 297    return str(ago % delta)298 299 300def _convert_aware_datetime(301    value: dt.datetime | dt.timedelta | float | None,302) -> Any:303    """Convert aware datetime to naive datetime and pass through any other type."""304    import datetime as dt305 306    if isinstance(value, dt.datetime) and value.tzinfo is not None:307        value = dt.datetime.fromtimestamp(value.timestamp())308    return value309 310 311def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str:312    """Return a natural day.313 314    For date values that are tomorrow, today or yesterday compared to315    present day return representing string. Otherwise, return a string316    formatted according to `format`.317 318    """319    import datetime as dt320 321    try:322        value = dt.date(value.year, value.month, value.day)323    except AttributeError:324        # Passed value wasn't date-ish325        return str(value)326    except (OverflowError, ValueError):327        # Date arguments out of range328        return str(value)329    delta = value - dt.date.today()330 331    if delta.days == 0:332        return _("today")333 334    if delta.days == 1:335        return _("tomorrow")336 337    if delta.days == -1:338        return _("yesterday")339 340    return value.strftime(format)341 342 343def naturaldate(value: dt.date | dt.datetime) -> str:344    """Like `naturalday`, but append a year for dates more than ~five months away."""345    import datetime as dt346 347    try:348        value = dt.date(value.year, value.month, value.day)349    except AttributeError:350        # Passed value wasn't date-ish351        return str(value)352    except (OverflowError, ValueError):353        # Date arguments out of range354        return str(value)355    delta = _abs_timedelta(value - dt.date.today())356    if delta.days >= 5 * 365 / 12:357        return naturalday(value, "%b %d %Y")358    return naturalday(value)359 360 361def _quotient_and_remainder(362    value: float,363    divisor: float,364    unit: Unit,365    minimum_unit: Unit,366    suppress: Iterable[Unit],367    format: str,368) -> tuple[float, float]:369    """Divide `value` by `divisor`, returning the quotient and remainder.370 371    If `unit` is `minimum_unit`, the quotient will be the rounding of `value / divisor`372    according to the `format` string and the remainder will be zero. The rationale is373    that if `unit` is the unit of the quotient, we cannot represent the remainder374    because it would require a unit smaller than the `minimum_unit`.375 376    >>> from humanize.time import _quotient_and_remainder, Unit377    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [], "%0.2f")378    (1.5, 0)379 380    If `unit` is in `suppress`, the quotient will be zero and the remainder will be the381    initial value. The idea is that if we cannot use `unit`, we are forced to use a382    lower unit, so we cannot do the division.383 384    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [Unit.DAYS], "%0.2f")385    (0, 36)386 387    In other cases, return the quotient and remainder as `divmod` would do it.388 389    >>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [], "%0.2f")390    (1, 12)391 392    """393    if unit == minimum_unit:394        return _rounding_by_fmt(format, value / divisor), 0395 396    if unit in suppress:397        return 0, value398 399    # Convert the remainder back to integer is necessary for months. 1 month is 30.5400    # days on average, but if we have 31 days, we want to count is as a whole month,401    # and not as 1 month plus a remainder of 0.5 days.402    q, r = divmod(value, divisor)403    return q, int(r)404 405 406def _suitable_minimum_unit(min_unit: Unit, suppress: Iterable[Unit]) -> Unit:407    """Return a minimum unit suitable that is not suppressed.408 409    If not suppressed, return the same unit:410 411    >>> from humanize.time import _suitable_minimum_unit, Unit412    >>> _suitable_minimum_unit(Unit.HOURS, []).name413    'HOURS'414 415    But if suppressed, find a unit greater than the original one that is not416    suppressed:417 418    >>> _suitable_minimum_unit(Unit.HOURS, [Unit.HOURS]).name419    'DAYS'420 421    >>> _suitable_minimum_unit(Unit.HOURS, [Unit.HOURS, Unit.DAYS]).name422    'MONTHS'423    """424    if min_unit in suppress:425        for unit in Unit:426            if unit > min_unit and unit not in suppress:427                return unit428 429        msg = "Minimum unit is suppressed and no suitable replacement was found"430        raise ValueError(msg)431 432    return min_unit433 434 435def _suppress_lower_units(min_unit: Unit, suppress: Iterable[Unit]) -> set[Unit]:436    """Extend suppressed units (if any) with all units lower than the minimum unit.437 438    >>> from humanize.time import _suppress_lower_units, Unit439    >>> [x.name for x in sorted(_suppress_lower_units(Unit.SECONDS, [Unit.DAYS]))]440    ['MICROSECONDS', 'MILLISECONDS', 'DAYS']441    """442    suppress = set(suppress)443    for unit in Unit:444        if unit == min_unit:445            break446        suppress.add(unit)447 448    return suppress449 450 451def precisedelta(452    value: dt.timedelta | float | None,453    minimum_unit: str = "seconds",454    suppress: Iterable[str] = (),455    format: str = "%0.2f",456) -> str:457    """Return a precise representation of a timedelta or number of seconds.458 459    ```pycon460    >>> import datetime as dt461    >>> from humanize.time import precisedelta462 463    >>> delta = dt.timedelta(seconds=3633, days=2, microseconds=123000)464    >>> precisedelta(delta)465    '2 days, 1 hour and 33.12 seconds'466 467    ```468 469    A custom `format` can be specified to control how the fractional part470    is represented:471 472    ```pycon473    >>> precisedelta(delta, format="%0.4f")474    '2 days, 1 hour and 33.1230 seconds'475 476    ```477 478    Instead, the `minimum_unit` can be changed to have a better resolution;479    the function will still readjust the unit to use the greatest of the480    units that does not lose precision.481 482    For example setting microseconds but still representing the date with milliseconds:483 484    ```pycon485    >>> precisedelta(delta, minimum_unit="microseconds")486    '2 days, 1 hour, 33 seconds and 123 milliseconds'487 488    ```489 490    If desired, some units can be suppressed: you will not see them represented and the491    time of the other units will be adjusted to keep representing the same timedelta:492 493    ```pycon494    >>> precisedelta(delta, suppress=['days'])495    '49 hours and 33.12 seconds'496 497    ```498 499    Note that microseconds precision is lost if the seconds and all500    the units below are suppressed:501 502    ```pycon503    >>> delta = dt.timedelta(seconds=90, microseconds=100)504    >>> precisedelta(delta, suppress=['seconds', 'milliseconds', 'microseconds'])505    '1.50 minutes'506 507    ```508 509    If the delta is too small to be represented with the minimum unit,510    a value of zero will be returned:511 512    ```pycon513    >>> delta = dt.timedelta(seconds=1)514    >>> precisedelta(delta, minimum_unit="minutes")515    '0.02 minutes'516 517    >>> delta = dt.timedelta(seconds=0.1)518    >>> precisedelta(delta, minimum_unit="minutes")519    '0 minutes'520 521    ```522    """523    date, delta = _date_and_delta(value, precise=True)524    if date is None:525        return str(value)526 527    suppress_set = {Unit[s.upper()] for s in suppress}528 529    # Find a suitable minimum unit (it can be greater than the one that the530    # user gave us, if that one is suppressed).531    min_unit = Unit[minimum_unit.upper()]532    min_unit = _suitable_minimum_unit(min_unit, suppress_set)533    del minimum_unit534 535    # Expand the suppressed units list/set to include all the units536    # that are below the minimum unit537    suppress_set = _suppress_lower_units(min_unit, suppress_set)538 539    # handy aliases540    days = delta.days541    secs = delta.seconds542    usecs = delta.microseconds543 544    MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS, MONTHS, YEARS = list(545        Unit546    )547 548    # Given DAYS compute YEARS and the remainder of DAYS as follows:549    #   if YEARS is the minimum unit, we cannot use DAYS so550    #   we will use a float for YEARS and 0 for DAYS:551    #       years, days = years/days, 0552    #553    #   if YEARS is suppressed, use DAYS:554    #       years, days = 0, days555    #556    #   otherwise:557    #       years, days = divmod(years, days)558    #559    # The same applies for months, hours, minutes and milliseconds below560    years, days = _quotient_and_remainder(561        days, 365, YEARS, min_unit, suppress_set, format562    )563    months, days = _quotient_and_remainder(564        days, 30.5, MONTHS, min_unit, suppress_set, format565    )566 567    secs = days * 24 * 3600 + secs568    days, secs = _quotient_and_remainder(569        secs, 24 * 3600, DAYS, min_unit, suppress_set, format570    )571 572    hours, secs = _quotient_and_remainder(573        secs, 3600, HOURS, min_unit, suppress_set, format574    )575    minutes, secs = _quotient_and_remainder(576        secs, 60, MINUTES, min_unit, suppress_set, format577    )578 579    usecs = secs * 1e6 + usecs580    secs, usecs = _quotient_and_remainder(581        usecs, 1e6, SECONDS, min_unit, suppress_set, format582    )583 584    msecs, usecs = _quotient_and_remainder(585        usecs, 1000, MILLISECONDS, min_unit, suppress_set, format586    )587 588    # Due to rounding, it could be that a unit is high enough to be promoted to a higher589    # unit. Example: 59.9 minutes was rounded to 60 minutes, and thus it should become 0590    # minutes and one hour more.591    if msecs >= 1_000 and SECONDS not in suppress_set:592        msecs -= 1_000593        secs += 1594    if secs >= 60 and MINUTES not in suppress_set:595        secs -= 60596        minutes += 1597    if minutes >= 60 and HOURS not in suppress_set:598        minutes -= 60599        hours += 1600    if hours >= 24 and DAYS not in suppress_set:601        hours -= 24602        days += 1603    # When adjusting we should not deal anymore with fractional days as all rounding has604    # been already made. We promote 31 days to an extra month.605    if days >= 31 and MONTHS not in suppress_set:606        days -= 31607        months += 1608    if months >= 12 and YEARS not in suppress_set:609        months -= 12610        years += 1611 612    fmts = [613        ("%d year", "%d years", years),614        ("%d month", "%d months", months),615        ("%d day", "%d days", days),616        ("%d hour", "%d hours", hours),617        ("%d minute", "%d minutes", minutes),618        ("%d second", "%d seconds", secs),619        ("%d millisecond", "%d milliseconds", msecs),620        ("%d microsecond", "%d microseconds", usecs),621    ]622 623    texts: list[str] = []624    for unit, fmt in zip(reversed(Unit), fmts):625        singular_txt, plural_txt, fmt_value = fmt626        if fmt_value > 0 or (not texts and unit == min_unit):627            _fmt_value = 2 if 1 < fmt_value < 2 else int(fmt_value)628            fmt_txt = _ngettext(singular_txt, plural_txt, _fmt_value)629            import math630 631            if unit == min_unit and math.modf(fmt_value)[0] > 0:632                fmt_txt = fmt_txt.replace("%d", format)633            elif unit == YEARS:634                if math.modf(fmt_value)[0] == 0:635                    fmt_value = int(fmt_value)636                fmt_txt = fmt_txt.replace("%d", "%s")637                texts.append(fmt_txt % intcomma(fmt_value))638                continue639 640            texts.append(fmt_txt % fmt_value)641 642        if unit == min_unit:643            break644 645    if len(texts) == 1:646        return texts[0]647 648    head = ", ".join(texts[:-1])649    tail = texts[-1]650 651    return _("%s and %s") % (head, tail)652 653 654def _rounding_by_fmt(format: str, value: float) -> float | int:655    """Round a number according to the string format provided.656 657    The string format is the old printf-style string formatting.658 659    If we are using a format which truncates the value, such as "%d" or "%i", the660    returned value will be of type `int`.661 662    If we are using a format which rounds the value, such as "%.2f" or even "%.0f",663    we will return a float.664    """665    result = format % value666 667    try:668        value = int(result)669    except ValueError:670        value = float(result)671 672    return value673 
codekingpro/portable-devtools · Team Ai