Skip to content

Commit

Permalink
Merge branch 'master' into dynamic-humanize
Browse files Browse the repository at this point in the history
  • Loading branch information
anishnya committed Oct 29, 2021
2 parents 995ee01 + baebfff commit e3a7f93
Show file tree
Hide file tree
Showing 16 changed files with 1,063 additions and 216 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/continuous_integration.yml
Expand Up @@ -16,12 +16,12 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3", "3.6", "3.7", "3.8", "3.9"]
python-version: ["pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10"]
os: [ubuntu-latest, macos-latest, windows-latest]
exclude:
# pypy3 randomly fails on Windows builds
- os: windows-latest
python-version: "pypy3"
python-version: "pypy-3.7"

steps:
# Check out latest code
Expand Down Expand Up @@ -73,7 +73,7 @@ jobs:

# Upload coverage report
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v2
with:
file: coverage.xml

Expand All @@ -85,10 +85,10 @@ jobs:
- uses: actions/checkout@v2

# Set up Python
- name: Set up Python 3.9
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: "3.9"
python-version: "3.10"

# Configure pip cache
- name: Cache pip
Expand Down
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Expand Up @@ -20,7 +20,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/asottile/pyupgrade
rev: v2.23.3
rev: v2.29.0
hooks:
- id: pyupgrade
args: [--py36-plus]
Expand All @@ -34,17 +34,17 @@ repos:
- id: rst-directive-colons
- id: rst-inline-touching-normal
- repo: https://github.com/psf/black
rev: 21.7b0
rev: 21.9b0
hooks:
- id: black
args: [--safe, --quiet, --target-version=py36]
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- repo: https://github.com/pycqa/flake8
rev: 4.0.1
hooks:
- id: flake8
additional_dependencies: [flake8-bugbear]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.910'
rev: 'v0.910-1'
hooks:
- id: mypy
additional_dependencies: [types-python-dateutil]
36 changes: 35 additions & 1 deletion CHANGELOG.rst
@@ -1,6 +1,40 @@
Changelog
=========

1.2.1 (2021-10-24)
------------------

- [NEW] Added quarter granularity to humanize, for example:

.. code-block:: python
>>> import arrow
>>> now = arrow.now()
>>> four_month_shift = now.shift(months=4)
>>> now.humanize(four_month_shift, granularity="quarter")
'a quarter ago'
>>> four_month_shift.humanize(now, granularity="quarter")
'in a quarter'
>>> thirteen_month_shift = now.shift(months=13)
>>> thirteen_month_shift.humanize(now, granularity="quarter")
'in 4 quarters'
>>> now.humanize(thirteen_month_shift, granularity="quarter")
'4 quarters ago'
- [NEW] Added Sinhala and Urdu locales.
- [NEW] Added official support for Python 3.10.
- [CHANGED] Updated Azerbaijani, Hebrew, and Serbian locales and added tests.
- [CHANGED] Passing an empty granularity list to ``humanize`` now raises a ``ValueError``.

1.2.0 (2021-09-12)
------------------

- [NEW] Added Albanian, Tamil and Zulu locales.
- [NEW] Added support for ``Decimal`` as input to ``arrow.get()``.
- [FIX] The Estonian, Finnish, Nepali and Zulu locales now support ``dehumanize``.
- [FIX] Improved validation checks when using parser tokens ``A`` and ``hh``.
- [FIX] Minor bug fixes to Catalan, Cantonese, Greek and Nepali locales.

1.1.1 (2021-06-24)
------------------

Expand Down Expand Up @@ -349,7 +383,7 @@ The following will work in v0.15.0:
-------------------

- [NEW] Added support for ``week`` granularity in ``Arrow.humanize()``. For example, ``arrow.utcnow().shift(weeks=-1).humanize(granularity="week")`` outputs "a week ago". This change introduced two new untranslated words, ``week`` and ``weeks``, to all locale dictionaries, so locale contributions are welcome!
- [NEW] Fully translated the Brazilian Portugese locale.
- [NEW] Fully translated the Brazilian Portuguese locale.
- [CHANGE] Updated the Macedonian locale to inherit from a Slavic base.
- [FIX] Fixed a bug that caused ``arrow.get()`` to ignore tzinfo arguments of type string (e.g. ``arrow.get(tzinfo="Europe/Paris")``).
- [FIX] Fixed a bug that occurred when ``arrow.Arrow()`` was instantiated with a ``pytz`` tzinfo object.
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Expand Up @@ -6,8 +6,9 @@ build36: PYTHON_VER = python3.6
build37: PYTHON_VER = python3.7
build38: PYTHON_VER = python3.8
build39: PYTHON_VER = python3.9
build310: PYTHON_VER = python3.10

build36 build37 build38 build39: clean
build36 build37 build38 build39 build310: clean
$(PYTHON_VER) -m venv venv
. venv/bin/activate; \
pip install -U pip setuptools wheel; \
Expand Down
2 changes: 1 addition & 1 deletion arrow/_version.py
@@ -1 +1 @@
__version__ = "1.1.1"
__version__ = "1.2.1"
94 changes: 60 additions & 34 deletions arrow/arrow.py
Expand Up @@ -75,6 +75,7 @@
"day",
"week",
"month",
"quarter",
"year",
]

Expand Down Expand Up @@ -132,6 +133,7 @@ class Arrow:
_SECS_PER_DAY: Final[int] = 60 * 60 * 24
_SECS_PER_WEEK: Final[int] = 60 * 60 * 24 * 7
_SECS_PER_MONTH: Final[float] = 60 * 60 * 24 * 30.5
_SECS_PER_QUARTER: Final[float] = 60 * 60 * 24 * 30.5 * 3
_SECS_PER_YEAR: Final[int] = 60 * 60 * 24 * 365

_SECS_MAP: Final[Mapping[TimeFrameLiteral, float]] = {
Expand All @@ -141,6 +143,7 @@ class Arrow:
"day": _SECS_PER_DAY,
"week": _SECS_PER_WEEK,
"month": _SECS_PER_MONTH,
"quarter": _SECS_PER_QUARTER,
"year": _SECS_PER_YEAR,
}

Expand Down Expand Up @@ -1246,19 +1249,28 @@ def humanize(
delta = sign * delta_second / self._SECS_PER_WEEK
elif granularity == "month":
delta = sign * delta_second / self._SECS_PER_MONTH
elif granularity == "quarter":
delta = sign * delta_second / self._SECS_PER_QUARTER
elif granularity == "year":
delta = sign * delta_second / self._SECS_PER_YEAR
else:
raise ValueError(
"Invalid level of granularity. "
"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'."
"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' or 'year'."
)

if trunc(abs(delta)) != 1:
granularity += "s" # type: ignore
return locale.describe(granularity, delta, only_distance=only_distance)

else:

if not granularity:
raise ValueError(
"Empty granularity list provided. "
"Please select one or more from 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'."
)

timeframes: List[Tuple[TimeFrameLiteral, float]] = []

def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float:
Expand All @@ -1281,6 +1293,7 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float:

frames: Tuple[TimeFrameLiteral, ...] = (
"year",
"quarter",
"month",
"week",
"day",
Expand All @@ -1294,7 +1307,7 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float:
if len(timeframes) < len(granularity) and not dynamic:
raise ValueError(
"Invalid level of granularity. "
"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'."
"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' or 'year'."
)

# Needed to see if there are no units output an error
Expand All @@ -1318,7 +1331,7 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float:
"Please consider making a contribution to this locale."
)

def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow":
def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, that represents
the time difference relative to the attrbiutes of the
:class:`Arrow <arrow.arrow.Arrow>` object.
Expand Down Expand Up @@ -1371,43 +1384,56 @@ def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow":
# Create a regex pattern object for numbers
num_pattern = re.compile(r"\d+")

# Search timestring for each time unit within locale
for unit in locale_obj.timeframes:
# Search input string for each time unit within locale
for unit, unit_object in locale_obj.timeframes.items():

# Numeric unit of change
change_value = 0
# Need to check the type of unit_object to create the correct dictionary
if isinstance(unit_object, Mapping):
strings_to_search = unit_object
else:
strings_to_search = {unit: str(unit_object)}

# Replace {0} with regex \d representing digits
search_string = str(locale_obj.timeframes[unit])
search_string = search_string.format(r"\d+")
# Search for any matches that exist for that locale's unit.
# Needs to cycle all through strings as some locales have strings that
# could overlap in a regex match, since input validation isn't being performed.
for time_delta, time_string in strings_to_search.items():

# Create search pattern and find within string
pattern = re.compile(fr"{search_string}")
match = pattern.search(timestring)
# Replace {0} with regex \d representing digits
search_string = str(time_string)
search_string = search_string.format(r"\d+")

# If there is no match continue to next iteration
if not match:
continue
# Create search pattern and find within string
pattern = re.compile(fr"{search_string}")
match = pattern.search(input_string)

match_string = match.group()
num_match = num_pattern.search(match_string)
# If there is no match continue to next iteration
if not match:
continue

# If no number matches set change value to be one
if not num_match:
change_value = 1
else:
change_value = int(num_match.group())
match_string = match.group()
num_match = num_pattern.search(match_string)

# No time to update if now is the unit
if unit == "now":
unit_visited[unit] = True
continue
# If no number matches
# Need for absolute value as some locales have signs included in their objects
if not num_match:
change_value = (
1 if not time_delta.isnumeric() else abs(int(time_delta))
)
else:
change_value = int(num_match.group())

# Add change value to the correct unit (incorporates the plurality that exists within timeframe i.e second v.s seconds)
time_unit_to_change = str(unit)
time_unit_to_change += "s" if (str(time_unit_to_change)[-1] != "s") else ""
time_object_info[time_unit_to_change] = change_value
unit_visited[time_unit_to_change] = True
# No time to update if now is the unit
if unit == "now":
unit_visited[unit] = True
continue

# Add change value to the correct unit (incorporates the plurality that exists within timeframe i.e second v.s seconds)
time_unit_to_change = str(unit)
time_unit_to_change += (
"s" if (str(time_unit_to_change)[-1] != "s") else ""
)
time_object_info[time_unit_to_change] = change_value
unit_visited[time_unit_to_change] = True

# Assert error if string does not modify any units
if not any([True for k, v in unit_visited.items() if v]):
Expand All @@ -1420,12 +1446,12 @@ def dehumanize(self, timestring: str, locale: str = "en_us") -> "Arrow":
future_string = locale_obj.future
future_string = future_string.format(".*")
future_pattern = re.compile(fr"^{future_string}$")
future_pattern_match = future_pattern.findall(timestring)
future_pattern_match = future_pattern.findall(input_string)

past_string = locale_obj.past
past_string = past_string.format(".*")
past_pattern = re.compile(fr"^{past_string}$")
past_pattern_match = past_pattern.findall(timestring)
past_pattern_match = past_pattern.findall(input_string)

# If a string contains the now unit, there will be no relative units, hence the need to check if the now unit
# was visited before raising a ValueError
Expand Down
12 changes: 12 additions & 0 deletions arrow/constants.py
Expand Up @@ -65,6 +65,8 @@
"se-se",
"sv",
"sv-se",
"fi",
"fi-fi",
"zh",
"zh-cn",
"zh-tw",
Expand Down Expand Up @@ -116,6 +118,10 @@
"sl-si",
"id",
"id-id",
"ne",
"ne-np",
"ee",
"et",
"sw",
"sw-ke",
"sw-tz",
Expand All @@ -130,7 +136,13 @@
"or-in",
"lb",
"lb-lu",
"zu",
"zu-za",
"sq",
"sq-al",
"ta",
"ta-in",
"ta-lk",
"ur",
"ur-pk",
}
3 changes: 3 additions & 0 deletions arrow/factory.py
Expand Up @@ -9,6 +9,7 @@
import calendar
from datetime import date, datetime
from datetime import tzinfo as dt_tzinfo
from decimal import Decimal
from time import struct_time
from typing import Any, List, Optional, Tuple, Type, Union, overload

Expand Down Expand Up @@ -218,6 +219,8 @@ def get(self, *args: Any, **kwargs: Any) -> Arrow:

if arg_count == 1:
arg = args[0]
if isinstance(arg, Decimal):
arg = float(arg)

# (None) -> raises an exception
if arg is None:
Expand Down

0 comments on commit e3a7f93

Please sign in to comment.