@@ -34,11 +34,13 @@
Loading
34 34
    def get(self, *args, **kwargs):
35 35
        """ Returns an :class:`Arrow <arrow.arrow.Arrow>` object based on flexible inputs.
36 36
37 -
        :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to
38 -
            'en_us'.
37 +
        :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en_us'.
39 38
        :param tzinfo: (optional) a :ref:`timezone expression <tz-expr>` or tzinfo object.
40 39
            Replaces the timezone unless using an input form that is explicitly UTC or specifies
41 40
            the timezone in a positional argument. Defaults to UTC.
41 +
        :param normalize_whitespace: (optional) a ``bool`` specifying whether or not to normalize
42 +
            redundant whitespace (spaces, tabs, and newlines) in a datetime string before parsing.
43 +
            Defaults to false.
42 44
43 45
        Usage::
44 46
@@ -141,6 +143,7 @@
Loading
141 143
        arg_count = len(args)
142 144
        locale = kwargs.pop("locale", "en_us")
143 145
        tz = kwargs.get("tzinfo", None)
146 +
        normalize_whitespace = kwargs.pop("normalize_whitespace", False)
144 147
145 148
        # if kwargs given, send to constructor unless only tzinfo provided
146 149
        if len(kwargs) > 1:
@@ -193,7 +196,7 @@
Loading
193 196
194 197
            # (str) -> parse.
195 198
            elif isstr(arg):
196 -
                dt = parser.DateTimeParser(locale).parse_iso(arg)
199 +
                dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace)
197 200
                return self.type.fromdatetime(dt, tz)
198 201
199 202
            # (struct_time) -> from struct_time
@@ -240,7 +243,9 @@
Loading
240 243
241 244
            # (str, format) -> parse.
242 245
            elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)):
243 -
                dt = parser.DateTimeParser(locale).parse(args[0], args[1])
246 +
                dt = parser.DateTimeParser(locale).parse(
247 +
                    args[0], args[1], normalize_whitespace
248 +
                )
244 249
                return self.type.fromdatetime(dt, tzinfo=tz)
245 250
246 251
            else:

@@ -8,6 +8,17 @@
Loading
8 8
9 9
from arrow import locales, util
10 10
11 +
FORMAT_ATOM = "YYYY-MM-DD HH:mm:ssZZ"
12 +
FORMAT_COOKIE = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ"
13 +
FORMAT_RFC822 = "ddd, DD MMM YY HH:mm:ss Z"
14 +
FORMAT_RFC850 = "dddd, DD-MMM-YY HH:mm:ss ZZZ"
15 +
FORMAT_RFC1036 = "ddd, DD MMM YY HH:mm:ss Z"
16 +
FORMAT_RFC1123 = "ddd, DD MMM YYYY HH:mm:ss Z"
17 +
FORMAT_RFC2822 = "ddd, DD MMM YYYY HH:mm:ss Z"
18 +
FORMAT_RFC3339 = "YYYY-MM-DD HH:mm:ssZZ"
19 +
FORMAT_RSS = "ddd, DD MMM YYYY HH:mm:ss Z"
20 +
FORMAT_W3C = "YYYY-MM-DD HH:mm:ssZZ"
21 +
11 22
12 23
class DateTimeFormatter(object):
13 24

@@ -1061,20 +1061,34 @@
Loading
1061 1061
1062 1062
    timeframes = {
1063 1063
        "now": "지금",
1064 -
        "second": "두 번째",
1065 -
        "seconds": "{0}몇 초",
1064 +
        "second": "1초",
1065 +
        "seconds": "{0}초",
1066 1066
        "minute": "1분",
1067 1067
        "minutes": "{0}분",
1068 -
        "hour": "1시간",
1068 +
        "hour": "한시간",
1069 1069
        "hours": "{0}시간",
1070 -
        "day": "1일",
1070 +
        "day": "하루",
1071 1071
        "days": "{0}일",
1072 -
        "month": "1개월",
1072 +
        "week": "1주",
1073 +
        "weeks": "{0}주",
1074 +
        "month": "한달",
1073 1075
        "months": "{0}개월",
1074 1076
        "year": "1년",
1075 1077
        "years": "{0}년",
1076 1078
    }
1077 1079
1080 +
    special_dayframes = {
1081 +
        -3: "그끄제",
1082 +
        -2: "그제",
1083 +
        -1: "어제",
1084 +
        1: "내일",
1085 +
        2: "모레",
1086 +
        3: "글피",
1087 +
        4: "그글피",
1088 +
    }
1089 +
1090 +
    special_yearframes = {-2: "제작년", -1: "작년", 1: "내년", 2: "내후년"}
1091 +
1078 1092
    month_names = [
1079 1093
        "",
1080 1094
        "1월",
@@ -1109,6 +1123,24 @@
Loading
1109 1123
    day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"]
1110 1124
    day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"]
1111 1125
1126 +
    def _ordinal_number(self, n):
1127 +
        ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"]
1128 +
        if n < len(ordinals):
1129 +
            return "{}번째".format(ordinals[n])
1130 +
        return "{}번째".format(n)
1131 +
1132 +
    def _format_relative(self, humanized, timeframe, delta):
1133 +
        if timeframe in ("day", "days"):
1134 +
            special = self.special_dayframes.get(delta)
1135 +
            if special:
1136 +
                return special
1137 +
        elif timeframe in ("year", "years"):
1138 +
            special = self.special_yearframes.get(delta)
1139 +
            if special:
1140 +
                return special
1141 +
1142 +
        return super(KoreanLocale, self)._format_relative(humanized, timeframe, delta)
1143 +
1112 1144
1113 1145
# derived locale types & implementations.
1114 1146
class DutchLocale(Locale):
@@ -1616,14 +1648,16 @@
Loading
1616 1648
1617 1649
    timeframes = {
1618 1650
        "now": "сега",
1619 -
        "second": "секунда",
1620 -
        "seconds": "{0} секунди",
1651 +
        "second": "една секунда",
1652 +
        "seconds": ["{0} секунда", "{0} секунди", "{0} секунди"],
1621 1653
        "minute": "една минута",
1622 1654
        "minutes": ["{0} минута", "{0} минути", "{0} минути"],
1623 1655
        "hour": "еден саат",
1624 1656
        "hours": ["{0} саат", "{0} саати", "{0} саати"],
1625 1657
        "day": "еден ден",
1626 1658
        "days": ["{0} ден", "{0} дена", "{0} дена"],
1659 +
        "week": "една недела",
1660 +
        "weeks": ["{0} недела", "{0} недели", "{0} недели"],
1627 1661
        "month": "еден месец",
1628 1662
        "months": ["{0} месец", "{0} месеци", "{0} месеци"],
1629 1663
        "year": "една година",
@@ -1649,39 +1683,39 @@
Loading
1649 1683
    ]
1650 1684
    month_abbreviations = [
1651 1685
        "",
1652 -
        "Јан.",
1653 -
        " Фев.",
1654 -
        " Мар.",
1655 -
        " Апр.",
1656 -
        " Мај",
1657 -
        " Јун.",
1658 -
        " Јул.",
1659 -
        " Авг.",
1660 -
        " Септ.",
1661 -
        " Окт.",
1662 -
        " Ноем.",
1663 -
        " Декем.",
1686 +
        "Јан",
1687 +
        "Фев",
1688 +
        "Мар",
1689 +
        "Апр",
1690 +
        "Мај",
1691 +
        "Јун",
1692 +
        "Јул",
1693 +
        "Авг",
1694 +
        "Септ",
1695 +
        "Окт",
1696 +
        "Ноем",
1697 +
        "Декем",
1664 1698
    ]
1665 1699
1666 1700
    day_names = [
1667 1701
        "",
1668 1702
        "Понеделник",
1669 -
        " Вторник",
1670 -
        " Среда",
1671 -
        " Четврток",
1672 -
        " Петок",
1673 -
        " Сабота",
1674 -
        " Недела",
1703 +
        "Вторник",
1704 +
        "Среда",
1705 +
        "Четврток",
1706 +
        "Петок",
1707 +
        "Сабота",
1708 +
        "Недела",
1675 1709
    ]
1676 1710
    day_abbreviations = [
1677 1711
        "",
1678 -
        "Пон.",
1679 -
        " Вт.",
1680 -
        " Сре.",
1681 -
        " Чет.",
1682 -
        " Пет.",
1683 -
        " Саб.",
1684 -
        " Нед.",
1712 +
        "Пон",
1713 +
        "Вт",
1714 +
        "Сре",
1715 +
        "Чет",
1716 +
        "Пет",
1717 +
        "Саб",
1718 +
        "Нед",
1685 1719
    ]
1686 1720
1687 1721
@@ -2833,6 +2867,8 @@
Loading
2833 2867
        "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodin"]},
2834 2868
        "day": {"past": "dnem", "future": "den", "zero": "{0} dnů"},
2835 2869
        "days": {"past": "{0} dny", "future": ["{0} dny", "{0} dnů"]},
2870 +
        "week": {"past": "týdnem", "future": "týden", "zero": "{0} týdnů"},
2871 +
        "weeks": {"past": "{0} týdny", "future": ["{0} týdny", "{0} týdnů"]},
2836 2872
        "month": {"past": "měsícem", "future": "měsíc", "zero": "{0} měsíců"},
2837 2873
        "months": {"past": "{0} měsíci", "future": ["{0} měsíce", "{0} měsíců"]},
2838 2874
        "year": {"past": "rokem", "future": "rok", "zero": "{0} let"},

@@ -2,6 +2,37 @@
Loading
2 2
from __future__ import absolute_import
3 3
4 4
import datetime
5 +
import numbers
6 +
7 +
from dateutil.rrule import WEEKLY, rrule
8 +
9 +
from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US
10 +
11 +
12 +
def next_weekday(start_date, weekday):
13 +
    """Get next weekday from the specified start date.
14 +
15 +
    :param start_date: Datetime object representing the start date.
16 +
    :param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday).
17 +
    :return: Datetime object corresponding to the next weekday after start_date.
18 +
19 +
    Usage::
20 +
21 +
        # Get first Monday after epoch
22 +
        >>> next_weekday(datetime(1970, 1, 1), 0)
23 +
        1970-01-05 00:00:00
24 +
25 +
        # Get first Thursday after epoch
26 +
        >>> next_weekday(datetime(1970, 1, 1), 3)
27 +
        1970-01-01 00:00:00
28 +
29 +
        # Get first Sunday after epoch
30 +
        >>> next_weekday(datetime(1970, 1, 1), 6)
31 +
        1970-01-04 00:00:00
32 +
    """
33 +
    if weekday < 0 or weekday > 6:
34 +
        raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).")
35 +
    return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0]
5 36
6 37
7 38
def total_seconds(td):
@@ -14,7 +45,9 @@
Loading
14 45
    if isinstance(value, bool):
15 46
        return False
16 47
    if not (
17 -
        isinstance(value, int) or isinstance(value, float) or isinstance(value, str)
48 +
        isinstance(value, numbers.Integral)
49 +
        or isinstance(value, float)
50 +
        or isinstance(value, str)
18 51
    ):
19 52
        return False
20 53
    try:
@@ -24,6 +57,20 @@
Loading
24 57
        return False
25 58
26 59
60 +
def normalize_timestamp(timestamp):
61 +
    """Normalize millisecond and microsecond timestamps into normal timestamps."""
62 +
    if timestamp > MAX_TIMESTAMP:
63 +
        if timestamp < MAX_TIMESTAMP_MS:
64 +
            timestamp /= 1e3
65 +
        elif timestamp < MAX_TIMESTAMP_US:
66 +
            timestamp /= 1e6
67 +
        else:
68 +
            raise ValueError(
69 +
                "The specified timestamp '{}' is too large.".format(timestamp)
70 +
            )
71 +
    return timestamp
72 +
73 +
27 74
# Credit to https://stackoverflow.com/a/1700069
28 75
def iso_to_gregorian(iso_year, iso_week, iso_day):
29 76
    """Converts an ISO week date tuple into a datetime object."""
@@ -58,4 +105,4 @@
Loading
58 105
        return isinstance(s, str)
59 106
60 107
61 -
__all__ = ["total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"]
108 +
__all__ = ["next_weekday", "total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"]

@@ -9,6 +9,7 @@
Loading
9 9
10 10
import calendar
11 11
import sys
12 +
import warnings
12 13
from datetime import datetime, timedelta
13 14
from datetime import tzinfo as dt_tzinfo
14 15
from math import trunc
@@ -18,6 +19,15 @@
Loading
18 19
19 20
from arrow import formatter, locales, parser, util
20 21
22 +
if sys.version_info[:2] < (3, 6):  # pragma: no cover
23 +
    with warnings.catch_warnings():
24 +
        warnings.simplefilter("default", DeprecationWarning)
25 +
        warnings.warn(
26 +
            "Arrow will drop support for Python 2.7 and 3.5 in the upcoming v1.0.0 release. Please upgrade to "
27 +
            "Python 3.6+ to continue receiving updates for Arrow.",
28 +
            DeprecationWarning,
29 +
        )
30 +
21 31
22 32
class Arrow(object):
23 33
    """An :class:`Arrow <arrow.arrow.Arrow>` object.
@@ -173,7 +183,8 @@
Loading
173 183
                "The provided timestamp '{}' is invalid.".format(timestamp)
174 184
            )
175 185
176 -
        dt = datetime.fromtimestamp(float(timestamp), tzinfo)
186 +
        timestamp = util.normalize_timestamp(float(timestamp))
187 +
        dt = datetime.fromtimestamp(timestamp, tzinfo)
177 188
178 189
        return cls(
179 190
            dt.year,
@@ -200,7 +211,8 @@
Loading
200 211
                "The provided timestamp '{}' is invalid.".format(timestamp)
201 212
            )
202 213
203 -
        dt = datetime.utcfromtimestamp(float(timestamp))
214 +
        timestamp = util.normalize_timestamp(float(timestamp))
215 +
        dt = datetime.utcfromtimestamp(timestamp)
204 216
205 217
        return cls(
206 218
            dt.year,

@@ -7,8 +7,7 @@
Loading
7 7
from dateutil import tz
8 8
9 9
from arrow import locales
10 -
from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US
11 -
from arrow.util import iso_to_gregorian
10 +
from arrow.util import iso_to_gregorian, next_weekday, normalize_timestamp
12 11
13 12
try:
14 13
    from functools import lru_cache
@@ -115,8 +114,11 @@
Loading
115 114
116 115
    # TODO: since we support more than ISO 8601, we should rename this function
117 116
    # IDEA: break into multiple functions
118 -
    def parse_iso(self, datetime_string):
119 -
        # TODO: add a flag to normalize whitespace (useful in logs, ref issue #421)
117 +
    def parse_iso(self, datetime_string, normalize_whitespace=False):
118 +
119 +
        if normalize_whitespace:
120 +
            datetime_string = re.sub(r"\s+", " ", datetime_string.strip())
121 +
120 122
        has_space_divider = " " in datetime_string
121 123
        has_t_divider = "T" in datetime_string
122 124
@@ -214,7 +216,10 @@
Loading
214 216
215 217
        return self._parse_multiformat(datetime_string, formats)
216 218
217 -
    def parse(self, datetime_string, fmt):
219 +
    def parse(self, datetime_string, fmt, normalize_whitespace=False):
220 +
221 +
        if normalize_whitespace:
222 +
            datetime_string = re.sub(r"\s+", " ", datetime_string)
218 223
219 224
        if isinstance(fmt, list):
220 225
            return self._parse_multiformat(datetime_string, fmt)
@@ -339,9 +344,15 @@
Loading
339 344
        elif token in ["DD", "D"]:
340 345
            parts["day"] = int(value)
341 346
342 -
        elif token in ["Do"]:
347 +
        elif token == "Do":
343 348
            parts["day"] = int(value)
344 349
350 +
        elif token == "dddd":
351 +
            parts["day_of_week"] = self.locale.day_names.index(value) - 1
352 +
353 +
        elif token == "ddd":
354 +
            parts["day_of_week"] = self.locale.day_abbreviations.index(value) - 1
355 +
345 356
        elif token.upper() in ["HH", "H"]:
346 357
            parts["hour"] = int(value)
347 358
@@ -414,20 +425,9 @@
Loading
414 425
        expanded_timestamp = parts.get("expanded_timestamp")
415 426
416 427
        if expanded_timestamp is not None:
417 -
418 -
            if expanded_timestamp > MAX_TIMESTAMP:
419 -
                if expanded_timestamp < MAX_TIMESTAMP_MS:
420 -
                    expanded_timestamp /= 1000.0
421 -
                elif expanded_timestamp < MAX_TIMESTAMP_US:
422 -
                    expanded_timestamp /= 1000000.0
423 -
                else:
424 -
                    raise ValueError(
425 -
                        "The specified timestamp '{}' is too large.".format(
426 -
                            expanded_timestamp
427 -
                        )
428 -
                    )
429 -
430 -
            return datetime.fromtimestamp(expanded_timestamp, tz=tz.tzutc())
428 +
            return datetime.fromtimestamp(
429 +
                normalize_timestamp(expanded_timestamp), tz=tz.tzutc(),
430 +
            )
431 431
432 432
        day_of_year = parts.get("day_of_year")
433 433
@@ -456,6 +456,24 @@
Loading
456 456
            parts["month"] = dt.month
457 457
            parts["day"] = dt.day
458 458
459 +
        day_of_week = parts.get("day_of_week")
460 +
        day = parts.get("day")
461 +
462 +
        # If day is passed, ignore day of week
463 +
        if day_of_week is not None and day is None:
464 +
            year = parts.get("year", 1970)
465 +
            month = parts.get("month", 1)
466 +
            day = 1
467 +
468 +
            # dddd => first day of week after epoch
469 +
            # dddd YYYY => first day of week in specified year
470 +
            # dddd MM YYYY => first day of week in specified year and month
471 +
            # dddd MM => first day after epoch in specified month
472 +
            next_weekday_dt = next_weekday(datetime(year, month, day), day_of_week)
473 +
            parts["year"] = next_weekday_dt.year
474 +
            parts["month"] = next_weekday_dt.month
475 +
            parts["day"] = next_weekday_dt.day
476 +
459 477
        am_pm = parts.get("am_pm")
460 478
        hour = parts.get("hour", 0)
461 479

@@ -1, +1, @@
Loading
1 -
__version__ = "0.15.6"
1 +
__version__ = "0.15.8"
Files Coverage
arrow 100.00%
Project Totals (9 files) 100.00%
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading