Repository: crsmithdev/arrow
Pull request #742 (2019-12-29 08:39:02)

Added Hong Kong (zh_hk) locale
## Pull Request Checklist

<!-- Check boxes by placing an x in the brackets: [x] -->
- [x] 🧪 Added **tests** for changed code.
- [x] 🛠️ All tests **pass** when run locally (run `tox` or `make test` to find out!).
- [x] 📚 Updated **documentation** for changed code.
- [x] ⏩ Code is **up-to-date** with the `master` branch.

## Description of Changes

Added Hong Kong (zh_hk) locale with its unit test

codecov-io: # [Codecov](https://codecov.io/gh/crsmithdev/arrow/pull/742?src=pr&el=h1) Report
> Merging [#742](https://codecov.io/gh/crsmithdev/arrow/pull/742?src=pr&el=desc) into [master](https://codecov.io/gh/crsmithdev/arrow/commit/70fb9053af26aeff455f889e11ba5db272f64633?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/crsmithdev/arrow/pull/742/graphs/tree.svg?width=650&token=4EkGnFsIGn&height=150&src=pr)](https://codecov.io/gh/crsmithdev/arrow/pull/742?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #742   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           9      9           
  Lines        1678   1687    +9     
  Branches      283    283           
=====================================
+ Hits         1678   1687    +9
```


| [Impacted Files](https://codecov.io/gh/crsmithdev/arrow/pull/742?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [arrow/locales.py](https://codecov.io/gh/crsmithdev/arrow/pull/742/diff?src=pr&el=tree#diff-YXJyb3cvbG9jYWxlcy5weQ==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/crsmithdev/arrow/pull/742?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/crsmithdev/arrow/pull/742?src=pr&el=footer). Last update [70fb905...a75f3bb](https://codecov.io/gh/crsmithdev/arrow/pull/742?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/arrow/arrow.py b/arrow/arrow.py
index 28e0b4c..89c3e50 100644
--- a/arrow/arrow.py
+++ b/arrow/arrow.py
@@ -162,7 +162,7 @@ class Arrow(object):
                 "The provided timestamp '{}' is invalid.".format(timestamp)
             )
 
-        dt = util.safe_fromtimestamp(float(timestamp), tzinfo)
+        dt = datetime.fromtimestamp(float(timestamp), tzinfo)
 
         return cls(
             dt.year,
@@ -188,7 +188,7 @@ class Arrow(object):
                 "The provided timestamp '{}' is invalid.".format(timestamp)
             )
 
-        dt = util.safe_utcfromtimestamp(float(timestamp))
+        dt = datetime.utcfromtimestamp(float(timestamp))
 
         return cls(
             dt.year,
diff --git a/arrow/locales.py b/arrow/locales.py
index 34d4bda..2c12cd9 100644
--- a/arrow/locales.py
+++ b/arrow/locales.py
@@ -932,6 +932,66 @@ class ChineseTWLocale(Locale):
     day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"]
 
 
+class HongKongLocale(Locale):
+
+    names = ["zh_hk"]
+
+    past = "{0}前"
+    future = "{0}後"
+
+    timeframes = {
+        "now": "剛才",
+        "second": "1秒",
+        "seconds": "{0}秒",
+        "minute": "1分鐘",
+        "minutes": "{0}分鐘",
+        "hour": "1小時",
+        "hours": "{0}小時",
+        "day": "1天",
+        "days": "{0}天",
+        "week": "1星期",
+        "weeks": "{0}星期",
+        "month": "1個月",
+        "months": "{0}個月",
+        "year": "1年",
+        "years": "{0}年",
+    }
+
+    month_names = [
+        "",
+        "1月",
+        "2月",
+        "3月",
+        "4月",
+        "5月",
+        "6月",
+        "7月",
+        "8月",
+        "9月",
+        "10月",
+        "11月",
+        "12月",
+    ]
+    month_abbreviations = [
+        "",
+        " 1",
+        " 2",
+        " 3",
+        " 4",
+        " 5",
+        " 6",
+        " 7",
+        " 8",
+        " 9",
+        "10",
+        "11",
+        "12",
+    ]
+
+    day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
+    day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"]
+
+
 class KoreanLocale(Locale):
 
     names = ["ko", "ko_kr"]
diff --git a/arrow/parser.py b/arrow/parser.py
index 5f8c07b..379c687 100644
--- a/arrow/parser.py
+++ b/arrow/parser.py
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
 
 from dateutil import tz
 
-from arrow import locales, util
+from arrow import locales
 from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US
 
 try:
@@ -383,7 +383,7 @@ class DateTimeParser(object):
         timestamp = parts.get("timestamp")
 
         if timestamp is not None:
-            return util.safe_fromtimestamp(timestamp, tz=tz.tzutc())
+            return datetime.fromtimestamp(timestamp, tz=tz.tzutc())
 
         expanded_timestamp = parts.get("expanded_timestamp")
 
@@ -401,7 +401,7 @@ class DateTimeParser(object):
                         )
                     )
 
-            return util.safe_fromtimestamp(expanded_timestamp, tz=tz.tzutc())
+            return datetime.fromtimestamp(expanded_timestamp, tz=tz.tzutc())
 
         day_of_year = parts.get("day_of_year")
 
diff --git a/arrow/util.py b/arrow/util.py
index cf3bf5b..62f1a05 100644
--- a/arrow/util.py
+++ b/arrow/util.py
@@ -2,11 +2,6 @@
 from __future__ import absolute_import
 
 import datetime
-import math
-import time
-from os import name as os_name
-
-import dateutil
 
 
 def total_seconds(td):  # pragma: no cover
@@ -28,44 +23,6 @@ def is_timestamp(value):
         return False
 
 
-def windows_datetime_from_timestamp(timestamp, tz=None):
-    """Computes datetime from timestamp. Supports negative timestamps on Windows platform."""
-    sec_frac, sec = math.modf(timestamp)
-    dt = datetime.datetime(1970, 1, 1, tzinfo=dateutil.tz.tzutc()) + datetime.timedelta(
-        seconds=sec, microseconds=sec_frac * 1000000
-    )
-    if tz is None:
-        tz = dateutil.tz.tzlocal()
-
-    if tz == dateutil.tz.tzlocal():
-        # because datetime.astimezone does not work on Windows for tzlocal() and dates before the 1970-01-01
-        # take timestamp from appropriate time of the year, because of daylight saving time changes
-        ts = time.mktime(dt.replace(year=1970).timetuple())
-        dt += datetime.datetime.fromtimestamp(ts) - datetime.datetime.utcfromtimestamp(
-            ts
-        )
-        dt = dt.replace(tzinfo=dateutil.tz.tzlocal())
-    else:
-        dt = dt.astimezone(tz)
-    return dt
-
-
-def safe_utcfromtimestamp(timestamp):
-    """ datetime.utcfromtimestamp alternative which supports negative timestamps on Windows platform."""
-    if os_name == "nt" and timestamp < 0:
-        return windows_datetime_from_timestamp(timestamp, dateutil.tz.tzutc())
-    else:
-        return datetime.datetime.utcfromtimestamp(timestamp)
-
-
-def safe_fromtimestamp(timestamp, tz=None):
-    """ datetime.fromtimestamp alternative which supports negative timestamps on Windows platform."""
-    if os_name == "nt" and timestamp < 0:
-        return windows_datetime_from_timestamp(timestamp, tz)
-    else:
-        return datetime.datetime.fromtimestamp(timestamp, tz)
-
-
 # Credit to https://stackoverflow.com/a/1700069
 def iso_to_gregorian(iso_year, iso_week, iso_day):
     """Converts an ISO week date tuple into a datetime object."""
@@ -100,12 +57,4 @@ except NameError:  # pragma: no cover
         return isinstance(s, str)
 
 
-__all__ = [
-    "total_seconds",
-    "is_timestamp",
-    "isstr",
-    "iso_to_gregorian",
-    "windows_datetime_from_timestamp",
-    "safe_utcfromtimestamp",
-    "safe_fromtimestamp",
-]
+__all__ = ["total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"]


Test patch:
diff --git a/tests/locales_tests.py b/tests/locales_tests.py
index 86f6f5f..83e5664 100644
--- a/tests/locales_tests.py
+++ b/tests/locales_tests.py
@@ -928,3 +928,27 @@ class BrazilianLocaleTests(Chai):
         self.assertEqual(self.locale._format_timeframe("months", 11), "11 meses")
         self.assertEqual(self.locale._format_timeframe("year", 1), "um ano")
         self.assertEqual(self.locale._format_timeframe("years", 12), "12 anos")
+
+
+class HongKongLocaleTests(Chai):
+    def setUp(self):
+        super(HongKongLocaleTests, self).setUp()
+
+        self.locale = locales.HongKongLocale()
+
+    def test_format_timeframe(self):
+        self.assertEqual(self.locale._format_timeframe("now", 0), "剛才")
+        self.assertEqual(self.locale._format_timeframe("second", 1), "1秒")
+        self.assertEqual(self.locale._format_timeframe("seconds", 30), "30秒")
+        self.assertEqual(self.locale._format_timeframe("minute", 1), "1分鐘")
+        self.assertEqual(self.locale._format_timeframe("minutes", 40), "40分鐘")
+        self.assertEqual(self.locale._format_timeframe("hour", 1), "1小時")
+        self.assertEqual(self.locale._format_timeframe("hours", 23), "23小時")
+        self.assertEqual(self.locale._format_timeframe("day", 1), "1天")
+        self.assertEqual(self.locale._format_timeframe("days", 12), "12天")
+        self.assertEqual(self.locale._format_timeframe("week", 1), "1星期")
+        self.assertEqual(self.locale._format_timeframe("weeks", 38), "38星期")
+        self.assertEqual(self.locale._format_timeframe("month", 1), "1個月")
+        self.assertEqual(self.locale._format_timeframe("months", 11), "11個月")
+        self.assertEqual(self.locale._format_timeframe("year", 1), "1年")
+        self.assertEqual(self.locale._format_timeframe("years", 12), "12年")
diff --git a/tests/parser_tests.py b/tests/parser_tests.py
index 08c20d1..c72da04 100644
--- a/tests/parser_tests.py
+++ b/tests/parser_tests.py
@@ -2,13 +2,14 @@
 from __future__ import unicode_literals
 
 import calendar
+import os
 import time
 from datetime import datetime
 
 from chai import Chai
 from dateutil import tz
 
-from arrow import parser, util
+from arrow import parser
 from arrow.constants import MAX_TIMESTAMP_US
 from arrow.parser import DateTimeParser, ParserError, ParserMatchError
 
@@ -227,19 +228,23 @@ class DateTimeParserParseTests(Chai):
             self.parser.parse("{:f}123456".format(float_timestamp), "X"), self.expected
         )
 
-        # regression test for issue #662
-        negative_int_timestamp = -int_timestamp
-        self.expected = util.safe_fromtimestamp(negative_int_timestamp, tz=tz_utc)
-        self.assertEqual(
-            self.parser.parse("{:d}".format(negative_int_timestamp), "X"), self.expected
-        )
+        # NOTE: negative timestamps cannot be handled by datetime on Window
+        # Must use timedelta to handle them. ref: https://stackoverflow.com/questions/36179914
+        if os.name != "nt":
+            # regression test for issue #662
+            negative_int_timestamp = -int_timestamp
+            self.expected = datetime.fromtimestamp(negative_int_timestamp, tz=tz_utc)
+            self.assertEqual(
+                self.parser.parse("{:d}".format(negative_int_timestamp), "X"),
+                self.expected,
+            )
 
-        negative_float_timestamp = -float_timestamp
-        self.expected = util.safe_fromtimestamp(negative_float_timestamp, tz=tz_utc)
-        self.assertEqual(
-            self.parser.parse("{:f}".format(negative_float_timestamp), "X"),
-            self.expected,
-        )
+            negative_float_timestamp = -float_timestamp
+            self.expected = datetime.fromtimestamp(negative_float_timestamp, tz=tz_utc)
+            self.assertEqual(
+                self.parser.parse("{:f}".format(negative_float_timestamp), "X"),
+                self.expected,
+            )
 
         # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will
         # break cases like "15 Jul 2000" and a format list (see issue #447)
diff --git a/tests/util_tests.py b/tests/util_tests.py
index 980a0a9..bb84c44 100644
--- a/tests/util_tests.py
+++ b/tests/util_tests.py
@@ -1,10 +1,7 @@
 # -*- coding: utf-8 -*-
 import time
-from datetime import datetime
 
 from chai import Chai
-from dateutil import tz
-from mock import patch
 
 from arrow import util
 
@@ -36,63 +33,3 @@ class UtilTests(Chai):
 
         with self.assertRaises(ValueError):
             util.iso_to_gregorian(2013, 8, 0)
-
-    def test_windows_datetime_from_timestamp(self):
-        timestamp = 1572204340.6460679
-        result = util.windows_datetime_from_timestamp(timestamp)
-        expected = datetime.fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal())
-        self.assertEqual(result, expected)
-
-    def test_windows_datetime_from_timestamp_utc(self):
-        timestamp = 1572204340.6460679
-        result = util.windows_datetime_from_timestamp(timestamp, tz.tzutc())
-        expected = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc())
-        self.assertEqual(result, expected)
-
-    def test_safe_utcfromtimestamp(self):
-        timestamp = 1572204340.6460679
-        result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc())
-        expected = datetime.utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc())
-        self.assertEqual(result, expected)
-
-    def test_safe_fromtimestamp_default_tz(self):
-        timestamp = 1572204340.6460679
-        result = util.safe_fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal())
-        expected = datetime.fromtimestamp(timestamp).replace(tzinfo=tz.tzlocal())
-        self.assertEqual(result, expected)
-
-    def test_safe_fromtimestamp_paris_tz(self):
-        timestamp = 1572204340.6460679
-        result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris"))
-        expected = datetime.fromtimestamp(timestamp, tz.gettz("Europe/Paris"))
-        self.assertEqual(result, expected)
-
-    def test_safe_utcfromtimestamp_negative(self):
-        timestamp = -1572204340.6460679
-        result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc())
-        expected = datetime(1920, 3, 7, 4, 34, 19, 353932, tzinfo=tz.tzutc())
-        self.assertEqual(result, expected)
-
-    def test_safe_fromtimestamp_negative(self):
-        timestamp = -1572204340.6460679
-        result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris"))
-        expected = datetime(
-            1920, 3, 7, 5, 34, 19, 353932, tzinfo=tz.gettz("Europe/Paris")
-        )
-        self.assertEqual(result, expected)
-
-    @patch.object(util, "os_name", "nt")
-    def test_safe_utcfromtimestamp_negative_nt(self):
-        timestamp = -1572204340.6460679
-        result = util.safe_utcfromtimestamp(timestamp).replace(tzinfo=tz.tzutc())
-        expected = datetime(1920, 3, 7, 4, 34, 19, 353932, tzinfo=tz.tzutc())
-        self.assertEqual(result, expected)
-
-    @patch.object(util, "os_name", "nt")
-    def test_safe_fromtimestamp_negative_nt(self):
-        timestamp = -1572204340.6460679
-        result = util.safe_fromtimestamp(timestamp, tz.gettz("Europe/Paris"))
-        expected = datetime(
-            1920, 3, 7, 5, 34, 19, 353932, tzinfo=tz.gettz("Europe/Paris")
-        )
-        self.assertEqual(result, expected)