Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apply PREFER_DAY_OF_MONTH in date.py #611

Merged
merged 2 commits into from
Mar 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions dateparser/date.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import calendar
import collections
from datetime import datetime, timedelta
from warnings import warn
Expand All @@ -15,7 +14,8 @@
from dateparser.languages.loader import LocaleDataLoader
from dateparser.conf import apply_settings
from dateparser.timezone_parser import pop_tz_offset_from_string
from dateparser.utils import apply_timezone_from_settings
from dateparser.utils import apply_timezone_from_settings, \
set_correct_day_from_settings

try:
# Python 3
Expand Down Expand Up @@ -128,10 +128,6 @@ def get_date_from_timestamp(date_string, settings):
return date_obj


def get_last_day_of_month(year, month):
return calendar.monthrange(year, month)[1]


def parse_with_formats(date_string, date_formats, settings):
""" Parse with formats and return a dictionary with 'period' and 'obj_date'.

Expand All @@ -148,12 +144,9 @@ def parse_with_formats(date_string, date_formats, settings):
except ValueError:
continue
else:
# If format does not include the day, use last day of the month
# instead of first, because the first is usually out of range.
if '%d' not in date_format:
period = 'month'
date_obj = date_obj.replace(
day=get_last_day_of_month(date_obj.year, date_obj.month))
date_obj = set_correct_day_from_settings(date_obj, settings)

if not ('%y' in date_format or '%Y' in date_format):
today = datetime.today()
Expand Down
20 changes: 7 additions & 13 deletions dateparser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from datetime import datetime
from datetime import timedelta

from dateparser.utils import set_correct_day_from_settings, \
get_last_day_of_month
from dateparser.utils.strptime import strptime


Expand Down Expand Up @@ -301,8 +303,7 @@ def _get_datetime_obj(self, **params):
(error_msgs[0] in error_text or error_msgs[1] in error_text) and
not(self._token_day or hasattr(self, '_token_weekday'))
):
_, tail = calendar.monthrange(params['year'], params['month'])
params['day'] = tail
params['day'] = get_last_day_of_month(params['year'], params['month'])
return datetime(**params)
else:
raise e
Expand Down Expand Up @@ -429,17 +430,10 @@ def _correct_for_day(self, dateobj):
):
return dateobj

_, tail = calendar.monthrange(dateobj.year, dateobj.month)
options = {
'first': 1,
'last': tail,
'current': self.now.day
}

try:
return dateobj.replace(day=options[self.settings.PREFER_DAY_OF_MONTH])
except ValueError:
return dateobj.replace(day=options['last'])
dateobj = set_correct_day_from_settings(
dateobj, self.settings, current_day=self.now.day
)
return dateobj

@classmethod
def parse(cls, datestring, settings):
Expand Down
20 changes: 20 additions & 0 deletions dateparser/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
import calendar
import logging
import types
import unicodedata
from datetime import datetime

import regex as re
from tzlocal import get_localzone
Expand Down Expand Up @@ -133,6 +135,24 @@ def apply_timezone_from_settings(date_obj, settings):
return date_obj


def get_last_day_of_month(year, month):
return calendar.monthrange(year, month)[1]


def set_correct_day_from_settings(date_obj, settings, current_day=None):
""" Set correct day attending the `PREFER_DAY_OF_MONTH` setting."""
noviluni marked this conversation as resolved.
Show resolved Hide resolved
options = {
'first': 1,
'last': get_last_day_of_month(date_obj.year, date_obj.month),
'current': current_day or datetime.now().day
}

try:
return date_obj.replace(day=options[settings.PREFER_DAY_OF_MONTH])
except ValueError:
return date_obj.replace(day=options['last'])


def registry(cls):
def choose(creator):
def constructor(cls, *args, **kwargs):
Expand Down
30 changes: 21 additions & 9 deletions tests/test_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import unittest
from collections import OrderedDict
from copy import copy
from datetime import datetime, timedelta

from mock import Mock, patch
Expand All @@ -12,7 +13,6 @@

import dateparser
from dateparser import date
from dateparser.date import get_last_day_of_month
from dateparser.conf import settings

from tests import BaseTestCase
Expand Down Expand Up @@ -274,18 +274,29 @@ def test_should_use_current_year_for_dates_without_year(

@parameterized.expand([
param(date_string='August 2014', date_formats=['%B %Y'],
expected_year=2014, expected_month=8),
expected_year=2014, expected_month=8, today_day=12,
prefer_day_of_month='first', expected_day=1),
param(date_string='August 2014', date_formats=['%B %Y'],
expected_year=2014, expected_month=8, today_day=12,
prefer_day_of_month='last', expected_day=31),
param(date_string='August 2014', date_formats=['%B %Y'],
expected_year=2014, expected_month=8, today_day=12,
prefer_day_of_month='current', expected_day=12),
])
def test_should_use_last_day_of_month_for_dates_without_day(
self, date_string, date_formats, expected_year, expected_month
def test_should_use_correct_day_from_settings_for_dates_without_day(
self, date_string, date_formats, expected_year, expected_month,
today_day, prefer_day_of_month, expected_day
):
self.given_now(2014, 8, 12)
self.when_date_is_parsed_with_formats(date_string, date_formats)
self.given_now(2014, 8, today_day)
settings_mod = copy(settings)
settings_mod.PREFER_DAY_OF_MONTH = prefer_day_of_month
self.when_date_is_parsed_with_formats(date_string, date_formats, settings_mod)
self.then_date_was_parsed()
self.then_parsed_period_is('month')
self.then_parsed_date_is(datetime(year=expected_year,
month=expected_month,
day=get_last_day_of_month(expected_year, expected_month)))
day=expected_day))


@parameterized.expand([
param(date_string='25-03-14', date_formats='%d-%m-%y', expected_result=datetime(2014, 3, 25)),
Expand All @@ -303,9 +314,10 @@ def given_now(self, year, month, day, **time):
datetime_mock.now = Mock(return_value=now)
datetime_mock.today = Mock(return_value=now)
self.add_patch(patch('dateparser.date.datetime', new=datetime_mock))
self.add_patch(patch('dateparser.utils.datetime', new=datetime_mock))

def when_date_is_parsed_with_formats(self, date_string, date_formats):
self.result = date.parse_with_formats(date_string, date_formats, settings)
def when_date_is_parsed_with_formats(self, date_string, date_formats, custom_settings=None):
self.result = date.parse_with_formats(date_string, date_formats, custom_settings or settings)

def then_date_was_not_parsed(self):
self.assertIsNotNone(self.result)
Expand Down
24 changes: 22 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from parameterized import parameterized, param
from dateparser.utils import (
find_date_separator, localize_timezone, apply_timezone,
apply_timezone_from_settings, registry
)
apply_timezone_from_settings, registry,
get_last_day_of_month)
from pytz import UnknownTimeZoneError, utc
from dateparser.conf import settings

Expand Down Expand Up @@ -104,3 +104,23 @@ def test_apply_timezone_from_settings_function_should_return_tz(self, date):
def test_registry_when_get_keys_not_implemented(self):
cl = self.make_class_without_get_keys()
self.assertRaises(NotImplementedError, registry, cl)

@parameterized.expand([
param(2111, 1, 31),
param(1999, 2, 28), # normal year
param(1996, 2, 29), # leap and not centurial year
param(2000, 2, 29), # leap and centurial year
param(1700, 2, 28), # no leap and centurial year (exception)
param(2020, 3, 31),
param(1987, 4, 30),
param(1000, 5, 31),
param(1534, 6, 30),
param(1777, 7, 31),
param(1234, 8, 31),
param(1678, 9, 30),
param(1947, 10, 31),
param(2015, 11, 30),
param(2300, 12, 31),
])
def test_get_last_day_of_month(self, year, month, expected_last_day):
assert get_last_day_of_month(year, month) == expected_last_day