humanize-4.10.0/.pre-commit-config.yaml 0000644 0000000 0000000 00000003204 13615410400 014614 0 ustar 00 repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: forbid-submodules
- id: trailing-whitespace
exclude: \.github/ISSUE_TEMPLATE\.md|\.github/PULL_REQUEST_TEMPLATE\.md
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.1
hooks:
- id: mypy
additional_dependencies: [pytest, types-freezegun, types-setuptools]
args: [--strict, --pretty, --show-error-codes, .]
pass_filenames: false
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 2.1.3
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.18
hooks:
- id: validate-pyproject
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.3.1
hooks:
- id: tox-ini-fmt
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
args: [--prose-wrap=always, --print-width=88]
exclude: \.github/ISSUE_TEMPLATE\.md|\.github/PULL_REQUEST_TEMPLATE\.md
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes
ci:
autoupdate_schedule: quarterly
humanize-4.10.0/.readthedocs.yml 0000644 0000000 0000000 00000000607 13615410400 013425 0 ustar 00 # Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Project page: https://readthedocs.org/projects/python-humanize/
version: 2
mkdocs:
configuration: mkdocs.yml
build:
os: ubuntu-22.04
tools:
python: "3"
commands:
- pip install -U tox
- tox -e docs
- mkdir _readthedocs
- mv site _readthedocs/html
humanize-4.10.0/RELEASING.md 0000644 0000000 0000000 00000003240 13615410400 012166 0 ustar 00 # Release Checklist
- [ ] Get `main` to the appropriate code release state.
[GitHub Actions](https://github.com/python-humanize/humanize/actions) should be
running cleanly for all merges to `main`.
[](https://github.com/python-humanize/humanize/actions)
* [ ] Start from a freshly cloned repo:
```bash
cd /tmp
rm -rf humanize
git clone https://github.com/python-humanize/humanize
cd humanize
# Generate translation binaries
scripts/generate-translation-binaries.sh
```
- [ ] (Optional) Create a distribution and release on **TestPyPI**:
```bash
pip install -U pip build keyring twine
rm -rf build dist
python -m build
twine check --strict dist/* && twine upload --repository testpypi dist/*
```
- [ ] (Optional) Check **test** installation:
```bash
pip3 uninstall -y humanize
pip3 install -U -i https://test.pypi.org/simple/ humanize --pre
python3 -c "import humanize; print(humanize.__version__)"
```
- [ ] Tag with the version number:
```bash
git tag -a 2.1.0 -m "Release 2.1.0"
```
- [ ] Create a distribution and release on **live PyPI**:
```bash
pip install -U pip build keyring twine
rm -rf build dist
python -m build
twine check --strict dist/* && twine upload --repository pypi dist/*
```
- [ ] Check installation:
```bash
pip uninstall -y humanize
pip install -U humanize
python3 -c "import humanize; print(humanize.__version__)"
```
- [ ] Push tag:
```bash
git push --tags
```
- [ ] Edit release draft, adjust text if needed:
https://github.com/python-humanize/humanize/releases
- [ ] Check next tag is correct, amend if needed
- [ ] Publish release
humanize-4.10.0/mkdocs.yml 0000644 0000000 0000000 00000001507 13615410400 012342 0 ustar 00 site_name: humanize
site_url: https://humanize.readthedocs.io
repo_url: https://github.com/python-humanize/humanize
theme:
name: material
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/toggle-switch
name: Switch to light mode
nav:
- Home: index.md
- Number: number.md
- Time: time.md
- Filesize: filesize.md
- Internationalisation: i18n.md
plugins:
- search
- mkdocstrings:
- include-markdown
markdown_extensions:
- pymdownx.highlight:
use_pygments: true
pygments_lang_class: true
- pymdownx.superfences
extra_css:
- css/code_select.css
watch:
- src/humanize
humanize-4.10.0/tox.ini 0000644 0000000 0000000 00000001215 13615410400 011646 0 ustar 00 [tox]
requires =
tox>=4.2
env_list =
docs
lint
py{py3, 313, 312, 311, 310, 39, 38}
[testenv]
extras =
tests
pass_env =
FORCE_COLOR
set_env =
COVERAGE_CORE = sysmon
commands =
{envpython} -m pytest \
--cov humanize \
--cov tests \
--cov-report html \
--cov-report term \
--cov-report xml \
{posargs}
[testenv:docs]
deps =
-r docs/requirements.txt
commands =
mkdocs build
[testenv:lint]
skip_install = true
deps =
pre-commit
pass_env =
PRE_COMMIT_COLOR
commands =
pre-commit run --all-files --show-diff-on-failure
[pytest]
addopts = --color=yes --doctest-modules
humanize-4.10.0/.github/CONTRIBUTING.md 0000644 0000000 0000000 00000001002 13615410400 014116 0 ustar 00 # Contributing
## Linting
Linting is run on the CI using [pre-commit](https://pre-commit.com/), and can be run
locally:
```sh
pip install pre-commit
pre-commit install # optional: to run when you commit, on just the staged changes
pre-commit run --all-files # to run on all files now
```
## Docstrings
Follow
[Google style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
for docstrings.
## Localization
See [README](https://github.com/python-humanize/humanize#localization).
humanize-4.10.0/.github/FUNDING.yml 0000644 0000000 0000000 00000000051 13615410400 013505 0 ustar 00 github: hugovk
tidelift: "pypi/humanize"
humanize-4.10.0/.github/ISSUE_TEMPLATE.md 0000644 0000000 0000000 00000000672 13615410400 014406 0 ustar 00 ### What did you do?
### What did you expect to happen?
### What actually happened?
### What versions are you using?
* OS:
* Python:
* Humanize:
Please include **code** that reproduces the issue.
The [best reproductions](https://stackoverflow.com/help/minimal-reproducible-example)
are
[self-contained scripts](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/)
with minimal dependencies.
```python
code goes here
```
humanize-4.10.0/.github/PULL_REQUEST_TEMPLATE.md 0000644 0000000 0000000 00000000072 13615410400 015474 0 ustar 00 Fixes #
Changes proposed in this pull request:
*
*
*
humanize-4.10.0/.github/SECURITY.md 0000644 0000000 0000000 00000000220 13615410400 013457 0 ustar 00 # Security policy
Security reports can be made via [Tidelift](https://tidelift.com/security). Tidelift
will coordinate the fix and disclosure.
humanize-4.10.0/.github/labels.yml 0000644 0000000 0000000 00000003636 13615410400 013671 0 ustar 00 # Default GitHub labels
- color: d73a4a
description: "Something isn't working"
name: bug
- color: cfd3d7
description: "This issue or pull request already exists"
name: duplicate
- color: a2eeef
description: "New feature or request"
name: enhancement
- color: 7057ff
description: "Good for newcomers"
name: good first issue
- color: 008672
description: "Extra attention is needed"
name: help wanted
- color: e4e669
description: "This doesn't seem right"
name: invalid
- color: d876e3
description: "Further information is requested"
name: question
- color: ffffff
description: "This will not be worked on"
name: wontfix
# Keep a Changelog labels
# https://keepachangelog.com/en/1.0.0/
- color: 0e8a16
description: "For new features"
name: "changelog: Added"
- color: af99e5
description: "For changes in existing functionality"
name: "changelog: Changed"
- color: FFA500
description: "For soon-to-be removed features"
name: "changelog: Deprecated"
- color: 00A800
description: "For any bug fixes"
name: "changelog: Fixed"
- color: ff0000
description: "For now removed features"
name: "changelog: Removed"
- color: 045aa0
description: "In case of vulnerabilities"
name: "changelog: Security"
- color: fbca04
description: "Exclude PR from release draft"
name: "changelog: skip"
# Other labels
- color: 0366d6
description: "For dependencies"
name: dependencies
- color: 0075ca
description: "Improvements or additions to documentation"
name: documentation
- color: d0c1ff
description: "Translations need updating"
name: "needs localisation"
- color: eb6123
description: ""
name: Hacktoberfest
- color: eb6123
description: "To credit accepted Hacktoberfest PRs"
name: hacktoberfest-accepted
- color: e29673
name: "needs tests"
- color: d65e88
description: "Deploy and release"
name: release
- color: fbca04
description: "Unit tests, linting, CI, etc."
name: testing
humanize-4.10.0/.github/release-drafter.yml 0000644 0000000 0000000 00000001574 13615410400 015473 0 ustar 00 name-template: "$RESOLVED_VERSION"
tag-template: "$RESOLVED_VERSION"
categories:
- title: "Added"
labels:
- "changelog: Added"
- "enhancement"
- title: "Changed"
label: "changelog: Changed"
- title: "Deprecated"
label: "changelog: Deprecated"
- title: "Removed"
label: "changelog: Removed"
- title: "Fixed"
labels:
- "changelog: Fixed"
- "bug"
- title: "Security"
label: "changelog: Security"
exclude-labels:
- "changelog: skip"
autolabeler:
- label: "changelog: skip"
branch:
- "/pre-commit-ci-update-config/"
template: |
$CHANGES
version-resolver:
major:
labels:
- "changelog: Removed"
minor:
labels:
- "changelog: Added"
- "changelog: Changed"
- "changelog: Deprecated"
- "enhancement"
patch:
labels:
- "changelog: Fixed"
- "bug"
default: minor
humanize-4.10.0/.github/renovate.json 0000644 0000000 0000000 00000000741 13615410400 014414 0 ustar 00 {
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", ":semanticCommitsDisabled"],
"labels": ["changelog: skip", "dependencies"],
"packageRules": [
{
"groupName": "github-actions",
"matchManagers": ["github-actions"],
"separateMajorMinor": "false"
},
{
"groupName": "docs/requirements.txt",
"matchPaths": ["docs/requirements.txt"]
}
],
"schedule": ["on the first day of the month"]
}
humanize-4.10.0/.github/workflows/docs.yml 0000644 0000000 0000000 00000000756 13615410400 015414 0 ustar 00 name: Docs
on: [push, pull_request, workflow_dispatch]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
cache-dependency-path: tox.ini
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U tox
- name: Docs
run: tox -e docs
humanize-4.10.0/.github/workflows/labels.yml 0000644 0000000 0000000 00000000605 13615410400 015717 0 ustar 00 name: Sync labels
permissions:
pull-requests: write
on:
push:
branches:
- main
paths:
- .github/labels.yml
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: micnncim/action-label-syncer@v1
with:
prune: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
humanize-4.10.0/.github/workflows/lint.yml 0000644 0000000 0000000 00000000501 13615410400 015416 0 ustar 00 name: Lint
on: [push, pull_request, workflow_dispatch]
env:
FORCE_COLOR: 1
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: pre-commit/action@v3.0.1
humanize-4.10.0/.github/workflows/release-drafter.yml 0000644 0000000 0000000 00000002105 13615410400 017517 0 ustar 00 name: Release drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- main
# pull_request event is required only for autolabeler
pull_request:
# Only following types are handled by the action, but one can default to all as well
types: [opened, reopened, synchronize]
# pull_request_target event is required for autolabeler to support PRs from forks
# pull_request_target:
# types: [opened, reopened, synchronize]
workflow_dispatch:
permissions:
contents: read
jobs:
update_release_draft:
if: github.repository_owner == 'python-humanize'
permissions:
# write permission is required to create a GitHub Release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest
steps:
# Drafts your next release notes as pull requests are merged into "main"
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
humanize-4.10.0/.github/workflows/require-pr-label.yml 0000644 0000000 0000000 00000001005 13615410400 017620 0 ustar 00 name: Require PR label
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: mheap/github-action-required-labels@v5
with:
mode: minimum
count: 1
labels:
"changelog: Added, changelog: Changed, changelog: Deprecated, changelog:
Fixed, changelog: Removed, changelog: Security, changelog: skip"
humanize-4.10.0/.github/workflows/test.yml 0000644 0000000 0000000 00000002772 13615410400 015443 0 ustar 00 name: Test
on: [push, pull_request, workflow_dispatch]
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
os: [windows-latest, macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip
- name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu')
run: |
sudo apt install gettext
- name: Install macOS dependencies
if: startsWith(matrix.os, 'macos')
run: |
brew install gettext
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U wheel
python -m pip install -U tox
- name: Generate translation binaries
run: |
scripts/generate-translation-binaries.sh
- name: Tox tests
run: |
tox -e py
- name: Upload coverage
uses: codecov/codecov-action@v3.1.5
with:
flags: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
success:
needs: test
runs-on: ubuntu-latest
name: Test successful
steps:
- name: Success
run: echo Test successful
humanize-4.10.0/docs/filesize.md 0000644 0000000 0000000 00000000042 13615410400 013414 0 ustar 00 # Filesize
::: humanize.filesize
humanize-4.10.0/docs/i18n.md 0000644 0000000 0000000 00000000052 13615410400 012362 0 ustar 00 # Internationalisation
::: humanize.i18n
humanize-4.10.0/docs/index.md 0000644 0000000 0000000 00000000400 13615410400 012707 0 ustar 00 # humanize
Welcome to the humanize API reference.
- [Number](number.md)
- [Time](time.md)
- [Filesize](filesize.md)
- [I18n](i18n.md)
{%
include-markdown "../README.md"
start=""
end=""
comments=false
%}
humanize-4.10.0/docs/number.md 0000644 0000000 0000000 00000000036 13615410400 013075 0 ustar 00 # Number
::: humanize.number
humanize-4.10.0/docs/requirements.txt 0000644 0000000 0000000 00000000176 13615410400 014554 0 ustar 00 mkdocs==1.6.0
mkdocs-material
mkdocstrings[python]==0.25.1
mkdocs-include-markdown-plugin
pygments
pymdown-extensions==10.8.1
humanize-4.10.0/docs/time.md 0000644 0000000 0000000 00000000032 13615410400 012537 0 ustar 00 # Time
::: humanize.time
humanize-4.10.0/docs/css/code_select.css 0000644 0000000 0000000 00000000375 13615410400 015044 0 ustar 00 /* Don't allow to select >>> in ```pycon``` code blocks */
.language-pycon .gp,
.language-pycon .go {
user-select: none;
}
/* Hide "Copy to clipboard" buttons in ```pycon``` code blocks */
.highlight.language-pycon .md-clipboard {
display: none;
}
humanize-4.10.0/scripts/generate-translation-binaries.sh 0000755 0000000 0000000 00000000314 13615410400 020300 0 ustar 00 set -e
for d in src/humanize/locale/*/; do
locale="$(basename $d)"
echo "$locale"
# compile to binary .mo
msgfmt --check -o src/humanize/locale/$locale/LC_MESSAGES/humanize{.mo,.po}
done
humanize-4.10.0/scripts/update-translations.sh 0000755 0000000 0000000 00000000721 13615410400 016363 0 ustar 00 set -e
# extract new phrases
xgettext --from-code=UTF-8 -o humanize.pot -k'_' -k'N_' -k'P_:1c,2' -k'NS_:1,2' -k'_ngettext:1,2' -l python src/humanize/*.py
for d in src/humanize/locale/*/; do
locale="$(basename $d)"
echo "$locale"
# add them to locale files
msgmerge -U src/humanize/locale/$locale/LC_MESSAGES/humanize.po humanize.pot
# compile to binary .mo
msgfmt --check -o src/humanize/locale/$locale/LC_MESSAGES/humanize{.mo,.po}
done
humanize-4.10.0/src/humanize/__init__.py 0000644 0000000 0000000 00000001600 13615410400 015051 0 ustar 00 """Main package for humanize."""
from __future__ import annotations
import importlib.metadata
from humanize.filesize import naturalsize
from humanize.i18n import activate, deactivate, decimal_separator, thousands_separator
from humanize.number import (
apnumber,
clamp,
fractional,
intcomma,
intword,
metric,
ordinal,
scientific,
)
from humanize.time import (
naturaldate,
naturalday,
naturaldelta,
naturaltime,
precisedelta,
)
__version__ = importlib.metadata.version(__name__)
__all__ = [
"__version__",
"activate",
"apnumber",
"clamp",
"deactivate",
"decimal_separator",
"fractional",
"intcomma",
"intword",
"metric",
"naturaldate",
"naturalday",
"naturaldelta",
"naturalsize",
"naturaltime",
"ordinal",
"precisedelta",
"scientific",
"thousands_separator",
]
humanize-4.10.0/src/humanize/filesize.py 0000644 0000000 0000000 00000004144 13615410400 015132 0 ustar 00 """Bits and bytes related humanization."""
from __future__ import annotations
suffixes = {
"decimal": (" kB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"),
"binary": (" KiB", " MiB", " GiB", " TiB", " PiB", " EiB", " ZiB", " YiB"),
"gnu": "KMGTPEZY",
}
def naturalsize(
value: float | str,
binary: bool = False,
gnu: bool = False,
format: str = "%.1f",
) -> str:
"""Format a number of bytes like a human readable filesize (e.g. 10 kB).
By default, decimal suffixes (kB, MB) are used.
Non-GNU modes are compatible with jinja2's `filesizeformat` filter.
Examples:
```pycon
>>> naturalsize(3000000)
'3.0 MB'
>>> naturalsize(300, False, True)
'300B'
>>> naturalsize(3000, False, True)
'2.9K'
>>> naturalsize(3000, False, True, "%.3f")
'2.930K'
>>> naturalsize(3000, True)
'2.9 KiB'
>>> naturalsize(10**28)
'10000.0 YB'
>>> naturalsize(-4096, True)
'-4.0 KiB'
```
Args:
value (int, float, str): Integer to convert.
binary (bool): If `True`, uses binary suffixes (KiB, MiB) with base
210 instead of 103.
gnu (bool): If `True`, the binary argument is ignored and GNU-style
(`ls -sh` style) prefixes are used (K, M) with the 2**10 definition.
format (str): Custom formatter.
Returns:
str: Human readable representation of a filesize.
"""
if gnu:
suffix = suffixes["gnu"]
elif binary:
suffix = suffixes["binary"]
else:
suffix = suffixes["decimal"]
base = 1024 if (gnu or binary) else 1000
bytes_ = float(value)
abs_bytes = abs(bytes_)
if abs_bytes == 1 and not gnu:
return "%d Byte" % bytes_
if abs_bytes < base and not gnu:
return "%d Bytes" % bytes_
if abs_bytes < base and gnu:
return "%dB" % bytes_
for i, s in enumerate(suffix):
unit = base ** (i + 2)
if abs_bytes < unit:
break
ret: str = format % (base * bytes_ / unit) + s
return ret
humanize-4.10.0/src/humanize/i18n.py 0000644 0000000 0000000 00000011223 13615410400 014073 0 ustar 00 """Activate, get and deactivate translations."""
from __future__ import annotations
import gettext as gettext_module
import os.path
from threading import local
__all__ = ["activate", "deactivate", "decimal_separator", "thousands_separator"]
_TRANSLATIONS: dict[str | None, gettext_module.NullTranslations] = {
None: gettext_module.NullTranslations()
}
_CURRENT = local()
# Mapping of locale to thousands separator
_THOUSANDS_SEPARATOR = {
"de_DE": ".",
"fr_FR": " ",
"it_IT": ".",
"pt_BR": ".",
"hu_HU": " ",
}
# Mapping of locale to decimal separator
_DECIMAL_SEPARATOR = {
"de_DE": ",",
"it_IT": ",",
"pt_BR": ",",
"hu_HU": ",",
}
def _get_default_locale_path() -> str | None:
try:
if __file__ is None:
return None
return os.path.join(os.path.dirname(__file__), "locale")
except NameError:
return None
def get_translation() -> gettext_module.NullTranslations:
try:
return _TRANSLATIONS[_CURRENT.locale]
except (AttributeError, KeyError):
return _TRANSLATIONS[None]
def activate(locale: str, path: str | None = None) -> gettext_module.NullTranslations:
"""Activate internationalisation.
Set `locale` as current locale. Search for locale in directory `path`.
Args:
locale (str): Language name, e.g. `en_GB`.
path (str): Path to search for locales.
Returns:
dict: Translations.
Raises:
Exception: If humanize cannot find the locale folder.
"""
if path is None:
path = _get_default_locale_path()
if path is None:
msg = (
"Humanize cannot determinate the default location of the 'locale' folder. "
"You need to pass the path explicitly."
)
raise Exception(msg)
if locale not in _TRANSLATIONS:
translation = gettext_module.translation("humanize", path, [locale])
_TRANSLATIONS[locale] = translation
_CURRENT.locale = locale
return _TRANSLATIONS[locale]
def deactivate() -> None:
"""Deactivate internationalisation."""
_CURRENT.locale = None
def _gettext(message: str) -> str:
"""Get translation.
Args:
message (str): Text to translate.
Returns:
str: Translated text.
"""
return get_translation().gettext(message)
def _pgettext(msgctxt: str, message: str) -> str:
"""Fetches a particular translation.
It works with `msgctxt` .po modifiers and allows duplicate keys with different
translations.
Args:
msgctxt (str): Context of the translation.
message (str): Text to translate.
Returns:
str: Translated text.
"""
return get_translation().pgettext(msgctxt, message)
def _ngettext(message: str, plural: str, num: int) -> str:
"""Plural version of _gettext.
Args:
message (str): Singular text to translate.
plural (str): Plural text to translate.
num (int): The number (e.g. item count) to determine translation for the
respective grammatical number.
Returns:
str: Translated text.
"""
return get_translation().ngettext(message, plural, num)
def _gettext_noop(message: str) -> str:
"""Mark a string as a translation string without translating it.
Example usage:
```python
CONSTANTS = [_gettext_noop('first'), _gettext_noop('second')]
def num_name(n):
return _gettext(CONSTANTS[n])
```
Args:
message (str): Text to translate in the future.
Returns:
str: Original text, unchanged.
"""
return message
def _ngettext_noop(singular: str, plural: str) -> tuple[str, str]:
"""Mark two strings as pluralized translations without translating them.
Example usage:
```python
CONSTANTS = [ngettext_noop('first', 'firsts'), ngettext_noop('second', 'seconds')]
def num_name(n):
return _ngettext(*CONSTANTS[n])
```
Args:
singular (str): Singular text to translate in the future.
plural (str): Plural text to translate in the future.
Returns:
tuple: Original text, unchanged.
"""
return singular, plural
def thousands_separator() -> str:
"""Return the thousands separator for a locale, default to comma.
Returns:
str: Thousands separator.
"""
try:
sep = _THOUSANDS_SEPARATOR[_CURRENT.locale]
except (AttributeError, KeyError):
sep = ","
return sep
def decimal_separator() -> str:
"""Return the decimal separator for a locale, default to dot.
Returns:
str: Decimal separator.
"""
try:
sep = _DECIMAL_SEPARATOR[_CURRENT.locale]
except (AttributeError, KeyError):
sep = "."
return sep
humanize-4.10.0/src/humanize/number.py 0000644 0000000 0000000 00000037561 13615410400 014621 0 ustar 00 """Humanizing functions for numbers."""
from __future__ import annotations
import math
import re
import sys
from fractions import Fraction
from typing import TYPE_CHECKING
from .i18n import _gettext as _
from .i18n import _ngettext, decimal_separator, thousands_separator
from .i18n import _ngettext_noop as NS_
from .i18n import _pgettext as P_
if TYPE_CHECKING:
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
# This type can be better defined by typing.SupportsInt, typing.SupportsFloat
# but that's a Python 3.8 only typing option.
NumberOrString: TypeAlias = "float | str"
def _format_not_finite(value: float) -> str:
"""Utility function to handle infinite and nan cases."""
if math.isnan(value):
return "NaN"
if math.isinf(value) and value < 0:
return "-Inf"
if math.isinf(value) and value > 0:
return "+Inf"
return ""
def ordinal(value: NumberOrString, gender: str = "male") -> str:
"""Converts an integer to its ordinal as a string.
For example, 1 is "1st", 2 is "2nd", 3 is "3rd", etc. Works for any integer or
anything `int()` will turn into an integer. Anything else will return the output
of str(value).
Examples:
```pycon
>>> ordinal(1)
'1st'
>>> ordinal(1002)
'1002nd'
>>> ordinal(103)
'103rd'
>>> ordinal(4)
'4th'
>>> ordinal(12)
'12th'
>>> ordinal(101)
'101st'
>>> ordinal(111)
'111th'
>>> ordinal("something else")
'something else'
>>> ordinal([1, 2, 3]) == "[1, 2, 3]"
True
```
Args:
value (int, str, float): Integer to convert.
gender (str): Gender for translations. Accepts either "male" or "female".
Returns:
str: Ordinal string.
"""
try:
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
value = int(value)
except (TypeError, ValueError):
return str(value)
if gender == "male":
t = (
P_("0 (male)", "th"),
P_("1 (male)", "st"),
P_("2 (male)", "nd"),
P_("3 (male)", "rd"),
P_("4 (male)", "th"),
P_("5 (male)", "th"),
P_("6 (male)", "th"),
P_("7 (male)", "th"),
P_("8 (male)", "th"),
P_("9 (male)", "th"),
)
else:
t = (
P_("0 (female)", "th"),
P_("1 (female)", "st"),
P_("2 (female)", "nd"),
P_("3 (female)", "rd"),
P_("4 (female)", "th"),
P_("5 (female)", "th"),
P_("6 (female)", "th"),
P_("7 (female)", "th"),
P_("8 (female)", "th"),
P_("9 (female)", "th"),
)
if value % 100 in (11, 12, 13): # special case
return f"{value}{t[0]}"
return f"{value}{t[value % 10]}"
def intcomma(value: NumberOrString, ndigits: int | None = None) -> str:
"""Converts an integer to a string containing commas every three digits.
For example, 3000 becomes "3,000" and 45000 becomes "45,000". To maintain some
compatibility with Django's `intcomma`, this function also accepts floats.
Examples:
```pycon
>>> intcomma(100)
'100'
>>> intcomma("1000")
'1,000'
>>> intcomma(1_000_000)
'1,000,000'
>>> intcomma(1_234_567.25)
'1,234,567.25'
>>> intcomma(1234.5454545, 2)
'1,234.55'
>>> intcomma(14308.40, 1)
'14,308.4'
>>> intcomma("14308.40", 1)
'14,308.4'
>>> intcomma(None)
'None'
```
Args:
value (int, float, str): Integer or float to convert.
ndigits (int, None): Digits of precision for rounding after the decimal point.
Returns:
str: String containing commas every three digits.
"""
thousands_sep = thousands_separator()
decimal_sep = decimal_separator()
try:
if isinstance(value, str):
value = value.replace(thousands_sep, "").replace(decimal_sep, ".")
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
if "." in value:
value = float(value)
else:
value = int(value)
else:
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
float(value)
except (TypeError, ValueError):
return str(value)
if ndigits is not None:
orig = "{0:.{1}f}".format(value, ndigits)
else:
orig = str(value)
orig = orig.replace(".", decimal_sep)
while True:
new = re.sub(r"^(-?\d+)(\d{3})", rf"\g<1>{thousands_sep}\g<2>", orig)
if orig == new:
return new
orig = new
powers = [10**x for x in (3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100)]
human_powers = (
NS_("thousand", "thousand"),
NS_("million", "million"),
NS_("billion", "billion"),
NS_("trillion", "trillion"),
NS_("quadrillion", "quadrillion"),
NS_("quintillion", "quintillion"),
NS_("sextillion", "sextillion"),
NS_("septillion", "septillion"),
NS_("octillion", "octillion"),
NS_("nonillion", "nonillion"),
NS_("decillion", "decillion"),
NS_("googol", "googol"),
)
def intword(value: NumberOrString, format: str = "%.1f") -> str:
"""Converts a large integer to a friendly text representation.
Works best for numbers over 1 million. For example, 1_000_000 becomes "1.0 million",
1200000 becomes "1.2 million" and "1_200_000_000" becomes "1.2 billion". Supports up
to decillion (33 digits) and googol (100 digits).
Examples:
```pycon
>>> intword("100")
'100'
>>> intword("12400")
'12.4 thousand'
>>> intword("1000000")
'1.0 million'
>>> intword(1_200_000_000)
'1.2 billion'
>>> intword(8100000000000000000000000000000000)
'8.1 decillion'
>>> intword(None)
'None'
>>> intword("1234000", "%0.3f")
'1.234 million'
```
Args:
value (int, float, str): Integer to convert.
format (str): To change the number of decimal or general format of the number
portion.
Returns:
str: Friendly text representation as a string, unless the value passed could not
be coaxed into an `int`.
"""
try:
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
value = int(value)
except (TypeError, ValueError):
return str(value)
if value < 0:
value *= -1
negative_prefix = "-"
else:
negative_prefix = ""
if value < powers[0]:
return negative_prefix + str(value)
for ordinal_, power in enumerate(powers[1:], 1):
if value < power:
chopped = value / float(powers[ordinal_ - 1])
powers_difference = powers[ordinal_] / powers[ordinal_ - 1]
if float(format % chopped) == powers_difference:
chopped = value / float(powers[ordinal_])
singular, plural = human_powers[ordinal_]
return (
negative_prefix
+ " ".join(
[format, _ngettext(singular, plural, math.ceil(chopped))]
)
) % chopped
singular, plural = human_powers[ordinal_ - 1]
return (
negative_prefix
+ " ".join([format, _ngettext(singular, plural, math.ceil(chopped))])
) % chopped
return negative_prefix + str(value)
def apnumber(value: NumberOrString) -> str:
"""Converts an integer to Associated Press style.
Examples:
```pycon
>>> apnumber(0)
'zero'
>>> apnumber(5)
'five'
>>> apnumber(10)
'10'
>>> apnumber("7")
'seven'
>>> apnumber("foo")
'foo'
>>> apnumber(None)
'None'
```
Args:
value (int, float, str): Integer to convert.
Returns:
str: For numbers 0-9, the number spelled out. Otherwise, the number. This always
returns a string unless the value was not `int`-able, then `str(value)`
is returned.
"""
try:
if not math.isfinite(float(value)):
return _format_not_finite(float(value))
value = int(value)
except (TypeError, ValueError):
return str(value)
if not 0 <= value < 10:
return str(value)
return (
_("zero"),
_("one"),
_("two"),
_("three"),
_("four"),
_("five"),
_("six"),
_("seven"),
_("eight"),
_("nine"),
)[value]
def fractional(value: NumberOrString) -> str:
"""Convert to fractional number.
There will be some cases where one might not want to show ugly decimal places for
floats and decimals.
This function returns a human-readable fractional number in form of fractions and
mixed fractions.
Pass in a string, or a number or a float, and this function returns:
* a string representation of a fraction
* or a whole number
* or a mixed fraction
* or the str output of the value, if it could not be converted
Examples:
```pycon
>>> fractional(0.3)
'3/10'
>>> fractional(1.3)
'1 3/10'
>>> fractional(float(1/3))
'1/3'
>>> fractional(1)
'1'
>>> fractional("ten")
'ten'
>>> fractional(None)
'None'
```
Args:
value (int, float, str): Integer to convert.
Returns:
str: Fractional number as a string.
"""
try:
number = float(value)
if not math.isfinite(number):
return _format_not_finite(number)
except (TypeError, ValueError):
return str(value)
whole_number = int(number)
frac = Fraction(number - whole_number).limit_denominator(1000)
numerator = frac.numerator
denominator = frac.denominator
if whole_number and not numerator and denominator == 1:
# this means that an integer was passed in
# (or variants of that integer like 1.0000)
return f"{whole_number:.0f}"
if not whole_number:
return f"{numerator:.0f}/{denominator:.0f}"
return f"{whole_number:.0f} {numerator:.0f}/{denominator:.0f}"
def scientific(value: NumberOrString, precision: int = 2) -> str:
"""Return number in string scientific notation z.wq x 10ⁿ.
Examples:
```pycon
>>> scientific(float(0.3))
'3.00 x 10⁻¹'
>>> scientific(int(500))
'5.00 x 10²'
>>> scientific(-1000)
'-1.00 x 10³'
>>> scientific(1000, 1)
'1.0 x 10³'
>>> scientific(1000, 3)
'1.000 x 10³'
>>> scientific("99")
'9.90 x 10¹'
>>> scientific("foo")
'foo'
>>> scientific(None)
'None'
```
Args:
value (int, float, str): Input number.
precision (int): Number of decimal for first part of the number.
Returns:
str: Number in scientific notation z.wq x 10ⁿ.
"""
exponents = {
"0": "⁰",
"1": "¹",
"2": "²",
"3": "³",
"4": "⁴",
"5": "⁵",
"6": "⁶",
"7": "⁷",
"8": "⁸",
"9": "⁹",
"-": "⁻",
}
try:
value = float(value)
if not math.isfinite(value):
return _format_not_finite(value)
except (ValueError, TypeError):
return str(value)
fmt = f"{{:.{str(int(precision))}e}}"
n = fmt.format(value)
part1, part2 = n.split("e")
# Remove redundant leading '+' or '0's (preserving the last '0' for 10⁰).
part2 = re.sub(r"^\+?(\-?)0*(.+)$", r"\1\2", part2)
new_part2 = []
for char in part2:
new_part2.append(exponents[char])
final_str = part1 + " x 10" + "".join(new_part2)
return final_str
def clamp(
value: float,
format: str = "{:}",
floor: float | None = None,
ceil: float | None = None,
floor_token: str = "<",
ceil_token: str = ">",
) -> str:
"""Returns number with the specified format, clamped between floor and ceil.
If the number is larger than ceil or smaller than floor, then the respective limit
will be returned, formatted and prepended with a token specifying as such.
Examples:
```pycon
>>> clamp(123.456)
'123.456'
>>> clamp(0.0001, floor=0.01)
'<0.01'
>>> clamp(0.99, format="{:.0%}", ceil=0.99)
'99%'
>>> clamp(0.999, format="{:.0%}", ceil=0.99)
'>99%'
>>> clamp(1, format=intword, floor=1e6, floor_token="under ")
'under 1.0 million'
>>> clamp(None) is None
True
```
Args:
value (int, float): Input number.
format (str OR callable): Can either be a formatting string, or a callable
function that receives value and returns a string.
floor (int, float): Smallest value before clamping.
ceil (int, float): Largest value before clamping.
floor_token (str): If value is smaller than floor, token will be prepended
to output.
ceil_token (str): If value is larger than ceil, token will be prepended
to output.
Returns:
str: Formatted number. The output is clamped between the indicated floor and
ceil. If the number is larger than ceil or smaller than floor, the output
will be prepended with a token indicating as such.
"""
if value is None:
return None
if not math.isfinite(value):
return _format_not_finite(value)
if floor is not None and value < floor:
value = floor
token = floor_token
elif ceil is not None and value > ceil:
value = ceil
token = ceil_token
else:
token = ""
if isinstance(format, str):
return token + format.format(value)
if callable(format):
return token + format(value)
msg = (
"Invalid format. Must be either a valid formatting string, or a function "
"that accepts value and returns a string."
)
raise ValueError(msg)
def metric(value: float, unit: str = "", precision: int = 3) -> str:
"""Return a value with a metric SI unit-prefix appended.
Examples:
```pycon
>>> metric(1500, "V")
'1.50 kV'
>>> metric(2e8, "W")
'200 MW'
>>> metric(220e-6, "F")
'220 μF'
>>> metric(1e-14, precision=4)
'10.00 f'
```
The unit prefix is always chosen so that non-significant zero digits are required.
i.e. `123,000` will become `123k` instead of `0.123M` and `1,230,000` will become
`1.23M` instead of `1230K`. For numbers that are either too huge or too tiny to
represent without resorting to either leading or trailing zeroes, it falls back to
`scientific()`.
```pycon
>>> metric(1e40)
'1.00 x 10⁴⁰'
```
Args:
value (int, float): Input number.
unit (str): Optional base unit.
precision (int): The number of digits the output should contain.
Returns:
str:
"""
if not math.isfinite(value):
return _format_not_finite(value)
exponent = int(math.floor(math.log10(abs(value)))) if value != 0 else 0
if exponent >= 33 or exponent < -30:
return scientific(value, precision - 1) + unit
value /= 10 ** (exponent // 3 * 3)
if exponent >= 3:
ordinal_ = "kMGTPEZYRQ"[exponent // 3 - 1]
elif exponent < 0:
ordinal_ = "mμnpfazyrq"[(-exponent - 1) // 3]
else:
ordinal_ = ""
value_ = format(value, ".%if" % max(0, precision - (exponent % 3) - 1))
if not (unit or ordinal_) or unit in ("°", "′", "″"):
space = ""
else:
space = " "
return f"{value_}{space}{ordinal_}{unit}"
humanize-4.10.0/src/humanize/py.typed 0000644 0000000 0000000 00000000000 13615410400 014430 0 ustar 00 humanize-4.10.0/src/humanize/time.py 0000644 0000000 0000000 00000044745 13615410400 014271 0 ustar 00 """Time humanizing functions.
These are largely borrowed from Django's `contrib.humanize`.
"""
from __future__ import annotations
import collections.abc
import datetime as dt
import math
import typing
from enum import Enum
from functools import total_ordering
from typing import Any
from .i18n import _gettext as _
from .i18n import _ngettext
from .number import intcomma
__all__ = [
"naturaldelta",
"naturaltime",
"naturalday",
"naturaldate",
"precisedelta",
]
@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: typing.Any) -> typing.Any:
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
def _now() -> dt.datetime:
return dt.datetime.now()
def _abs_timedelta(delta: dt.timedelta) -> dt.timedelta:
"""Return an "absolute" value for a timedelta, always representing a time distance.
Args:
delta (datetime.timedelta): Input timedelta.
Returns:
datetime.timedelta: Absolute timedelta.
"""
if delta.days < 0:
now = _now()
return now - (now + delta)
return delta
def _date_and_delta(
value: typing.Any, *, now: dt.datetime | None = None
) -> tuple[typing.Any, typing.Any]:
"""Turn a value into a date and a timedelta which represents how long ago it was.
If that's not possible, return `(None, value)`.
"""
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: dt.timedelta | float,
months: bool = True,
minimum_unit: str = "seconds",
) -> str:
"""Return a natural representation of a timedelta or number of seconds.
This is similar to `naturaltime`, but does not add tense to the result.
Args:
value (datetime.timedelta, int or float): A timedelta or a number of seconds.
months (bool): If `True`, then a number of months (based on 30.5 days) will be
used for fuzziness between years.
minimum_unit (str): The lowest unit that can be used.
Returns:
str (str or `value`): A natural representation of the amount of time
elapsed unless `value` is not datetime.timedelta or cannot be
converted to int (cannot be float due to 'inf' or 'nan').
In that case, a `value` is returned unchanged.
Raises:
OverflowError: If `value` is too large to convert to datetime.timedelta.
Examples:
Compare two timestamps in a custom local timezone::
import datetime as dt
from dateutil.tz import gettz
berlin = gettz("Europe/Berlin")
now = dt.datetime.now(tz=berlin)
later = now + dt.timedelta(minutes=30)
assert naturaldelta(later - now) == "30 minutes"
"""
tmp = Unit[minimum_unit.upper()]
if tmp not in (Unit.SECONDS, Unit.MILLISECONDS, Unit.MICROSECONDS):
msg = f"Minimum unit '{minimum_unit}' not supported"
raise ValueError(msg)
min_unit = tmp
if isinstance(value, dt.timedelta):
delta = value
else:
try:
int(value) # Explicitly don't support string such as "NaN" or "inf"
value = float(value)
delta = dt.timedelta(seconds=value)
except (ValueError, TypeError):
return str(value)
use_months = months
delta = abs(delta)
years = delta.days // 365
days = delta.days % 365
num_months = int(days // 30.5)
if not years and days < 1:
if delta.seconds == 0:
if min_unit == Unit.MICROSECONDS and delta.microseconds < 1000:
return (
_ngettext("%d microsecond", "%d microseconds", delta.microseconds)
% delta.microseconds
)
if min_unit == Unit.MILLISECONDS or (
min_unit == Unit.MICROSECONDS and 1000 <= delta.microseconds < 1_000_000
):
milliseconds = delta.microseconds / 1000
return (
_ngettext("%d millisecond", "%d milliseconds", int(milliseconds))
% milliseconds
)
return _("a moment")
if delta.seconds == 1:
return _("a second")
if delta.seconds < 60:
return _ngettext("%d second", "%d seconds", delta.seconds) % delta.seconds
if 60 <= delta.seconds < 120:
return _("a minute")
if 120 <= delta.seconds < 3600:
minutes = delta.seconds // 60
return _ngettext("%d minute", "%d minutes", minutes) % minutes
if 3600 <= delta.seconds < 3600 * 2:
return _("an hour")
if 3600 < delta.seconds:
hours = delta.seconds // 3600
return _ngettext("%d hour", "%d hours", hours) % hours
elif years == 0:
if days == 1:
return _("a day")
if not use_months:
return _ngettext("%d day", "%d days", days) % days
if not num_months:
return _ngettext("%d day", "%d days", days) % days
if num_months == 1:
return _("a month")
return _ngettext("%d month", "%d months", num_months) % num_months
elif years == 1:
if not num_months and not days:
return _("a year")
if not num_months:
return _ngettext("1 year, %d day", "1 year, %d days", days) % days
if use_months:
if num_months == 1:
return _("1 year, 1 month")
return (
_ngettext("1 year, %d month", "1 year, %d months", num_months)
% num_months
)
return _ngettext("1 year, %d day", "1 year, %d days", days) % days
return _ngettext("%d year", "%d years", years).replace("%d", "%s") % intcomma(years)
def naturaltime(
value: dt.datetime | dt.timedelta | float,
future: bool = False,
months: bool = True,
minimum_unit: str = "seconds",
when: dt.datetime | None = None,
) -> str:
"""Return a natural representation of a time in a resolution that makes sense.
This is more or less compatible with Django's `naturaltime` filter.
Args:
value (datetime.datetime, datetime.timedelta, int or float): A `datetime`, a
`timedelta`, or a number of seconds.
future (bool): Ignored for `datetime`s and `timedelta`s, where the tense is
always figured out based on the current time. For integers and floats, the
return value will be past tense by default, unless future is `True`.
months (bool): If `True`, then a number of months (based on 30.5 days) will be
used for fuzziness between years.
minimum_unit (str): The lowest unit that can be used.
when (datetime.datetime): Point in time relative to which _value_ is
interpreted. Defaults to the current time in the local timezone.
Returns:
str: A natural representation of the input in a resolution that makes sense.
"""
value = _convert_aware_datetime(value)
when = _convert_aware_datetime(when)
now = when or _now()
date, delta = _date_and_delta(value, now=now)
if date is None:
return str(value)
# determine tense by value only if datetime/timedelta were passed
if isinstance(value, (dt.datetime, dt.timedelta)):
future = date > now
ago = _("%s from now") if future else _("%s ago")
delta = naturaldelta(delta, months, minimum_unit)
if delta == _("a moment"):
return _("now")
return str(ago % delta)
def _convert_aware_datetime(
value: dt.datetime | dt.timedelta | float | None,
) -> Any:
"""Convert aware datetime to naive datetime and pass through any other type."""
if isinstance(value, dt.datetime) and value.tzinfo is not None:
value = dt.datetime.fromtimestamp(value.timestamp())
return value
def naturalday(value: dt.date | dt.datetime, format: str = "%b %d") -> str:
"""Return a natural day.
For date values that are tomorrow, today or yesterday compared to
present day return representing string. Otherwise, return a string
formatted according to `format`.
"""
try:
value = dt.date(value.year, value.month, value.day)
except AttributeError:
# Passed value wasn't date-ish
return str(value)
except (OverflowError, ValueError):
# Date arguments out of range
return str(value)
delta = value - dt.date.today()
if delta.days == 0:
return _("today")
if delta.days == 1:
return _("tomorrow")
if delta.days == -1:
return _("yesterday")
return value.strftime(format)
def naturaldate(value: dt.date | dt.datetime) -> str:
"""Like `naturalday`, but append a year for dates more than ~five months away."""
try:
value = dt.date(value.year, value.month, value.day)
except AttributeError:
# Passed value wasn't date-ish
return str(value)
except (OverflowError, ValueError):
# Date arguments out of range
return str(value)
delta = _abs_timedelta(value - dt.date.today())
if delta.days >= 5 * 365 / 12:
return naturalday(value, "%b %d %Y")
return naturalday(value)
def _quotient_and_remainder(
value: float,
divisor: float,
unit: Unit,
minimum_unit: Unit,
suppress: collections.abc.Iterable[Unit],
) -> tuple[float, float]:
"""Divide `value` by `divisor` returning the quotient and remainder.
If `unit` is `minimum_unit`, makes the quotient a float number and the remainder
will be zero. The rational is that if `unit` is the unit of the quotient, we cannot
represent the remainder because it would require a unit smaller than the
`minimum_unit`.
>>> from humanize.time import _quotient_and_remainder, Unit
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.DAYS, [])
(1.5, 0)
If unit is in `suppress`, the quotient will be zero and the remainder will be the
initial value. The idea is that if we cannot use `unit`, we are forced to use a
lower unit so we cannot do the division.
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [Unit.DAYS])
(0, 36)
In other case return quotient and remainder as `divmod` would do it.
>>> _quotient_and_remainder(36, 24, Unit.DAYS, Unit.HOURS, [])
(1, 12)
"""
if unit == minimum_unit:
return value / divisor, 0
if unit in suppress:
return 0, value
return divmod(value, divisor)
def _carry(
value1: float,
value2: float,
ratio: float,
unit: Unit,
min_unit: Unit,
suppress: typing.Iterable[Unit],
) -> tuple[float, float]:
"""Return a tuple with two values.
If the unit is in `suppress`, multiply `value1` by `ratio` and add it to `value2`
(carry to right). The idea is that if we cannot represent `value1` we need to
represent it in a lower unit.
>>> from humanize.time import _carry, Unit
>>> _carry(2, 6, 24, Unit.DAYS, Unit.SECONDS, [Unit.DAYS])
(0, 54)
If the unit is the minimum unit, `value2` is divided by `ratio` and added to
`value1` (carry to left). We assume that `value2` has a lower unit so we need to
carry it to `value1`.
>>> _carry(2, 6, 24, Unit.DAYS, Unit.DAYS, [])
(2.25, 0)
Otherwise, just return the same input:
>>> _carry(2, 6, 24, Unit.DAYS, Unit.SECONDS, [])
(2, 6)
"""
if unit == min_unit:
return value1 + value2 / ratio, 0
if unit in suppress:
return 0, value2 + value1 * ratio
return value1, value2
def _suitable_minimum_unit(min_unit: Unit, suppress: typing.Iterable[Unit]) -> Unit:
"""Return a minimum unit suitable that is not suppressed.
If not suppressed, return the same unit:
>>> from humanize.time import _suitable_minimum_unit, Unit
>>> _suitable_minimum_unit(Unit.HOURS, []).name
'HOURS'
But if suppressed, find a unit greater than the original one that is not
suppressed:
>>> _suitable_minimum_unit(Unit.HOURS, [Unit.HOURS]).name
'DAYS'
>>> _suitable_minimum_unit(Unit.HOURS, [Unit.HOURS, Unit.DAYS]).name
'MONTHS'
"""
if min_unit in suppress:
for unit in Unit:
if unit > min_unit and unit not in suppress:
return unit
msg = "Minimum unit is suppressed and no suitable replacement was found"
raise ValueError(msg)
return min_unit
def _suppress_lower_units(min_unit: Unit, suppress: typing.Iterable[Unit]) -> set[Unit]:
"""Extend suppressed units (if any) with all units lower than the minimum unit.
>>> from humanize.time import _suppress_lower_units, Unit
>>> [x.name for x in sorted(_suppress_lower_units(Unit.SECONDS, [Unit.DAYS]))]
['MICROSECONDS', 'MILLISECONDS', 'DAYS']
"""
suppress = set(suppress)
for unit in Unit:
if unit == min_unit:
break
suppress.add(unit)
return suppress
def precisedelta(
value: dt.timedelta | int | None,
minimum_unit: str = "seconds",
suppress: typing.Iterable[str] = (),
format: str = "%0.2f",
) -> str:
"""Return a precise representation of a timedelta.
```pycon
>>> import datetime as dt
>>> from humanize.time import precisedelta
>>> delta = dt.timedelta(seconds=3633, days=2, microseconds=123000)
>>> precisedelta(delta)
'2 days, 1 hour and 33.12 seconds'
```
A custom `format` can be specified to control how the fractional part
is represented:
```pycon
>>> precisedelta(delta, format="%0.4f")
'2 days, 1 hour and 33.1230 seconds'
```
Instead, the `minimum_unit` can be changed to have a better resolution;
the function will still readjust the unit to use the greatest of the
units that does not lose precision.
For example setting microseconds but still representing the date with milliseconds:
```pycon
>>> precisedelta(delta, minimum_unit="microseconds")
'2 days, 1 hour, 33 seconds and 123 milliseconds'
```
If desired, some units can be suppressed: you will not see them represented and the
time of the other units will be adjusted to keep representing the same timedelta:
```pycon
>>> precisedelta(delta, suppress=['days'])
'49 hours and 33.12 seconds'
```
Note that microseconds precision is lost if the seconds and all
the units below are suppressed:
```pycon
>>> delta = dt.timedelta(seconds=90, microseconds=100)
>>> precisedelta(delta, suppress=['seconds', 'milliseconds', 'microseconds'])
'1.50 minutes'
```
If the delta is too small to be represented with the minimum unit,
a value of zero will be returned:
```pycon
>>> delta = dt.timedelta(seconds=1)
>>> precisedelta(delta, minimum_unit="minutes")
'0.02 minutes'
>>> delta = dt.timedelta(seconds=0.1)
>>> precisedelta(delta, minimum_unit="minutes")
'0 minutes'
```
"""
date, delta = _date_and_delta(value)
if date is None:
return str(value)
suppress_set = {Unit[s.upper()] for s in suppress}
# Find a suitable minimum unit (it can be greater the one that the
# user gave us if it is suppressed).
min_unit = Unit[minimum_unit.upper()]
min_unit = _suitable_minimum_unit(min_unit, suppress_set)
del minimum_unit
# Expand the suppressed units list/set to include all the units
# that are below the minimum unit
suppress_set = _suppress_lower_units(min_unit, suppress_set)
# handy aliases
days = delta.days
secs = delta.seconds
usecs = delta.microseconds
MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS, MONTHS, YEARS = list(
Unit
)
# Given DAYS compute YEARS and the remainder of DAYS as follows:
# if YEARS is the minimum unit, we cannot use DAYS so
# we will use a float for YEARS and 0 for DAYS:
# years, days = years/days, 0
#
# if YEARS is suppressed, use DAYS:
# years, days = 0, days
#
# otherwise:
# years, days = divmod(years, days)
#
# The same applies for months, hours, minutes and milliseconds below
years, days = _quotient_and_remainder(days, 365, YEARS, min_unit, suppress_set)
months, days = _quotient_and_remainder(days, 30.5, MONTHS, min_unit, suppress_set)
# If DAYS is not in suppress, we can represent the days but
# if it is a suppressed unit, we need to carry it to a lower unit,
# seconds in this case.
#
# The same applies for secs and usecs below
days, secs = _carry(days, secs, 24 * 3600, DAYS, min_unit, suppress_set)
hours, secs = _quotient_and_remainder(secs, 3600, HOURS, min_unit, suppress_set)
minutes, secs = _quotient_and_remainder(secs, 60, MINUTES, min_unit, suppress_set)
secs, usecs = _carry(secs, usecs, 1e6, SECONDS, min_unit, suppress_set)
msecs, usecs = _quotient_and_remainder(
usecs, 1000, MILLISECONDS, min_unit, suppress_set
)
# if _unused != 0 we had lost some precision
usecs, _unused = _carry(usecs, 0, 1, MICROSECONDS, min_unit, suppress_set)
fmts = [
("%d year", "%d years", years),
("%d month", "%d months", months),
("%d day", "%d days", days),
("%d hour", "%d hours", hours),
("%d minute", "%d minutes", minutes),
("%d second", "%d seconds", secs),
("%d millisecond", "%d milliseconds", msecs),
("%d microsecond", "%d microseconds", usecs),
]
texts: list[str] = []
for unit, fmt in zip(reversed(Unit), fmts):
singular_txt, plural_txt, fmt_value = fmt
if fmt_value > 0 or (not texts and unit == min_unit):
_fmt_value = 2 if 1 < fmt_value < 2 else int(fmt_value)
fmt_txt = _ngettext(singular_txt, plural_txt, _fmt_value)
if unit == min_unit and math.modf(fmt_value)[0] > 0:
fmt_txt = fmt_txt.replace("%d", format)
elif unit == YEARS:
fmt_txt = fmt_txt.replace("%d", "%s")
texts.append(fmt_txt % intcomma(fmt_value))
continue
texts.append(fmt_txt % fmt_value)
if unit == min_unit:
break
if len(texts) == 1:
return texts[0]
head = ", ".join(texts[:-1])
tail = texts[-1]
return _("%s and %s") % (head, tail)
humanize-4.10.0/src/humanize/locale/ar/LC_MESSAGES/humanize.mo 0000644 0000000 0000000 00000007622 13615410400 020555 0 ustar 00 D < a \ ? T g |
"
0 >
J X
d r
~
) 0 8 H \ b g
l z ! % 7 = C L ^ b l m q
C ? S
" ) 2 - 9 - g
(
1
<
E
L
W
`
g
r
y
! 4 ) = g - p - % % ! (
8
C N ! W
y 5 <