persiantools 5.2.1__tar.gz → 5.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {persiantools-5.2.1 → persiantools-5.3.0}/LICENSE +1 -1
- {persiantools-5.2.1/persiantools.egg-info → persiantools-5.3.0}/PKG-INFO +1 -2
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools/__init__.py +1 -1
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools/jdatetime.py +133 -23
- {persiantools-5.2.1 → persiantools-5.3.0/persiantools.egg-info}/PKG-INFO +1 -2
- {persiantools-5.2.1 → persiantools-5.3.0}/setup.py +0 -1
- {persiantools-5.2.1 → persiantools-5.3.0}/tests/test_digits.py +11 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/tests/test_jalalidate.py +181 -2
- {persiantools-5.2.1 → persiantools-5.3.0}/tests/test_jalalidatetime.py +185 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/MANIFEST.in +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/README.md +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools/characters.py +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools/digits.py +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools/utils.py +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools.egg-info/SOURCES.txt +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools.egg-info/dependency_links.txt +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools.egg-info/not-zip-safe +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools.egg-info/requires.txt +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/persiantools.egg-info/top_level.txt +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/pyproject.toml +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/setup.cfg +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/tests/test_characters.py +0 -0
- {persiantools-5.2.1 → persiantools-5.3.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: persiantools
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.0
|
|
4
4
|
Summary: Jalali date and datetime with other tools
|
|
5
5
|
Home-page: https://github.com/majiidd/persiantools
|
|
6
6
|
Author: Majid Hajiloo
|
|
@@ -8,7 +8,6 @@ Author-email: majid.hajiloo@gmail.com
|
|
|
8
8
|
License: MIT
|
|
9
9
|
Keywords: jalali shamsi persian digits characters converter jalalidate jalalidatetime date datetime jdate jdatetime farsi
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
11
|
Classifier: Natural Language :: Persian
|
|
13
12
|
Classifier: Operating System :: OS Independent
|
|
14
13
|
Classifier: Programming Language :: Python
|
|
@@ -633,12 +633,13 @@ class JalaliDate:
|
|
|
633
633
|
raise ValueError(f"Invalid date separator: {dtstr[4]}")
|
|
634
634
|
|
|
635
635
|
month = int(dtstr[5:7])
|
|
636
|
-
|
|
637
636
|
if dtstr[7] != "-":
|
|
638
637
|
raise ValueError("Invalid date separator")
|
|
639
638
|
|
|
640
639
|
day = int(dtstr[8:10])
|
|
641
640
|
|
|
641
|
+
cls._check_date_fields(year, month, day, "en")
|
|
642
|
+
|
|
642
643
|
return [year, month, day]
|
|
643
644
|
|
|
644
645
|
def __hash__(self):
|
|
@@ -941,8 +942,122 @@ class JalaliDate:
|
|
|
941
942
|
raise NotImplementedError
|
|
942
943
|
|
|
943
944
|
@classmethod
|
|
944
|
-
def strptime(cls, data_string, fmt):
|
|
945
|
-
|
|
945
|
+
def strptime(cls, data_string, fmt, locale="en"):
|
|
946
|
+
if locale not in ["en", "fa"]:
|
|
947
|
+
raise ValueError("locale must be 'en' or 'fa'")
|
|
948
|
+
|
|
949
|
+
if locale == "fa":
|
|
950
|
+
data_string = digits.fa_to_en(data_string)
|
|
951
|
+
|
|
952
|
+
month_names_list = MONTH_NAMES_EN[1:] if locale == "en" else MONTH_NAMES_FA[1:]
|
|
953
|
+
month_names_abbr_list = MONTH_NAMES_ABBR_EN[1:] if locale == "en" else MONTH_NAMES_ABBR_FA[1:]
|
|
954
|
+
weekday_names_list = WEEKDAY_NAMES_EN if locale == "en" else WEEKDAY_NAMES_FA
|
|
955
|
+
weekday_names_abbr_list = WEEKDAY_NAMES_ABBR_EN if locale == "en" else WEEKDAY_NAMES_ABBR_FA
|
|
956
|
+
|
|
957
|
+
directives_regex_pattern = {
|
|
958
|
+
"%Y": r"(?P<Y>\d{4})",
|
|
959
|
+
"%y": r"(?P<y>\d{2})",
|
|
960
|
+
"%m": r"(?P<m>1[0-2]|0?[1-9])",
|
|
961
|
+
"%d": r"(?P<d>\d{1,2})",
|
|
962
|
+
"%b": cls._seqToRE(month_names_abbr_list, "b"),
|
|
963
|
+
"%B": cls._seqToRE(month_names_list, "B"),
|
|
964
|
+
"%a": cls._seqToRE(weekday_names_abbr_list, "a"),
|
|
965
|
+
"%A": cls._seqToRE(weekday_names_list, "A"),
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
fmt = utils.replace(
|
|
969
|
+
fmt,
|
|
970
|
+
{
|
|
971
|
+
"%x": "%Y/%m/%d",
|
|
972
|
+
"%c": "%a %b %d %Y",
|
|
973
|
+
},
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
data_string_regex = utils.replace(fmt, directives_regex_pattern)
|
|
977
|
+
full_pattern = f"^{data_string_regex}$"
|
|
978
|
+
|
|
979
|
+
match = re.match(full_pattern, data_string, re.IGNORECASE)
|
|
980
|
+
if not match:
|
|
981
|
+
match_strict = re.match(f"^{data_string_regex}$", data_string, re.IGNORECASE)
|
|
982
|
+
if not match_strict:
|
|
983
|
+
raise ValueError(f"Date string '{data_string}' does not match format '{fmt}'")
|
|
984
|
+
directives = match_strict.groupdict()
|
|
985
|
+
else:
|
|
986
|
+
directives = match.groupdict()
|
|
987
|
+
|
|
988
|
+
parsed_components = {}
|
|
989
|
+
for k, v in directives.items():
|
|
990
|
+
if v is not None:
|
|
991
|
+
if k not in ["a", "A", "b", "B"] and v.isdigit():
|
|
992
|
+
parsed_components[k] = int(v)
|
|
993
|
+
else:
|
|
994
|
+
parsed_components[k] = v
|
|
995
|
+
|
|
996
|
+
year = parsed_components.get("Y")
|
|
997
|
+
yy = parsed_components.get("y")
|
|
998
|
+
|
|
999
|
+
if year is None and yy is not None:
|
|
1000
|
+
if not (0 <= yy <= 99):
|
|
1001
|
+
raise ValueError(f"Year without century (yy) '{yy}' out of range 00-99.")
|
|
1002
|
+
# Heuristic: if yy > 70, assume 13yy, else 14yy.
|
|
1003
|
+
year = (
|
|
1004
|
+
(1300 + yy) if yy > (2070 - 2000) else (1400 + yy)
|
|
1005
|
+
) # Adjusted heuristic to be roughly 70 for 1300 century.
|
|
1006
|
+
# Current Jalali year is around 140x. So values like 01, 02.. up to e.g. 70 => 14xx.
|
|
1007
|
+
# values like 71, 72 .. 99 => 13xx.
|
|
1008
|
+
elif year is None:
|
|
1009
|
+
raise ValueError("Year information is missing from the date string or format.")
|
|
1010
|
+
|
|
1011
|
+
month = parsed_components.get("m")
|
|
1012
|
+
if month is None:
|
|
1013
|
+
month_name_abbr = parsed_components.get("b")
|
|
1014
|
+
month_name_full = parsed_components.get("B")
|
|
1015
|
+
|
|
1016
|
+
found_month = False
|
|
1017
|
+
if month_name_abbr is not None:
|
|
1018
|
+
normalized_month_abbr = month_name_abbr.capitalize() if locale == "en" else month_name_abbr
|
|
1019
|
+
try:
|
|
1020
|
+
month = month_names_abbr_list.index(normalized_month_abbr) + 1
|
|
1021
|
+
found_month = True
|
|
1022
|
+
except ValueError:
|
|
1023
|
+
try:
|
|
1024
|
+
month = month_names_list.index(normalized_month_abbr) + 1
|
|
1025
|
+
found_month = True
|
|
1026
|
+
except ValueError:
|
|
1027
|
+
pass
|
|
1028
|
+
|
|
1029
|
+
if not found_month and month_name_full is not None:
|
|
1030
|
+
normalized_month_full = month_name_full.capitalize() if locale == "en" else month_name_full
|
|
1031
|
+
try:
|
|
1032
|
+
month = month_names_list.index(normalized_month_full) + 1
|
|
1033
|
+
found_month = True
|
|
1034
|
+
except ValueError:
|
|
1035
|
+
pass
|
|
1036
|
+
|
|
1037
|
+
if not found_month:
|
|
1038
|
+
raise ValueError(
|
|
1039
|
+
f"Month name not recognized from '{month_name_abbr or month_name_full}' for locale '{locale}'."
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
day = parsed_components.get("d")
|
|
1043
|
+
if day is None:
|
|
1044
|
+
raise ValueError("Day information is missing from the date string or format.")
|
|
1045
|
+
|
|
1046
|
+
cls._check_date_fields(year, month, day, locale)
|
|
1047
|
+
|
|
1048
|
+
return cls(year, month, day, locale=locale)
|
|
1049
|
+
|
|
1050
|
+
@staticmethod
|
|
1051
|
+
def _seqToRE(to_convert, directive):
|
|
1052
|
+
to_convert = sorted(to_convert, key=len, reverse=True)
|
|
1053
|
+
for value in to_convert:
|
|
1054
|
+
if value != "":
|
|
1055
|
+
break
|
|
1056
|
+
else:
|
|
1057
|
+
return ""
|
|
1058
|
+
regex = "|".join(re_escape(stuff) for stuff in to_convert)
|
|
1059
|
+
regex = f"(?P<{directive}>{regex}"
|
|
1060
|
+
return "%s)" % regex
|
|
946
1061
|
|
|
947
1062
|
|
|
948
1063
|
_tzinfo_class = tzinfo
|
|
@@ -1604,21 +1719,26 @@ class JalaliDateTime(JalaliDate):
|
|
|
1604
1719
|
look for the table under "strftime() and strptime() Format Codes" section.
|
|
1605
1720
|
"""
|
|
1606
1721
|
directives_regex_pattern = {
|
|
1607
|
-
"%Y": r"(?P<Y>\d
|
|
1722
|
+
"%Y": r"(?P<Y>\d{4})",
|
|
1608
1723
|
"%m": r"(?P<m>1[0-2]|0[1-9]|[1-9])",
|
|
1609
1724
|
"%d": r"(?P<d>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
|
|
1610
|
-
"%a": cls.
|
|
1611
|
-
"%A": cls.
|
|
1612
|
-
"%b": cls.
|
|
1613
|
-
"%B": cls.
|
|
1725
|
+
"%a": cls._seqToRE(weekday_names_abbr, "a"),
|
|
1726
|
+
"%A": cls._seqToRE(weekday_names, "A"),
|
|
1727
|
+
"%b": cls._seqToRE(month_names_abbr, "b"),
|
|
1728
|
+
"%B": cls._seqToRE(month_names, "B"),
|
|
1614
1729
|
"%H": r"(?P<H>2[0-3]|[0-1]\d|\d)",
|
|
1615
1730
|
"%I": r"(?P<I>1[0-2]|0[1-9]|[1-9])",
|
|
1616
|
-
"%p": cls.
|
|
1731
|
+
"%p": cls._seqToRE(periods, "p"),
|
|
1617
1732
|
"%M": r"(?P<M>[0-5]\d|\d)",
|
|
1618
1733
|
"%S": r"(?P<S>6[0-1]|[0-5]\d|\d)",
|
|
1619
1734
|
"%f": r"(?P<f>\d{1,6})",
|
|
1620
|
-
"%z":
|
|
1621
|
-
|
|
1735
|
+
"%z": (
|
|
1736
|
+
r"(?P<z>[-+](?P<zH>2[0-3]|[0-1]\d)"
|
|
1737
|
+
r"(?:[:]?)(?P<zM>[0-5]\d)"
|
|
1738
|
+
r"(?:[:]?(?P<zS>[0-5]\d))?"
|
|
1739
|
+
r"(?:\.(?P<zf>\d{1,6}))?)"
|
|
1740
|
+
),
|
|
1741
|
+
"%Z": cls._seqToRE(pytz.all_timezones, "Z"),
|
|
1622
1742
|
}
|
|
1623
1743
|
|
|
1624
1744
|
fmt = utils.replace(
|
|
@@ -1631,8 +1751,9 @@ class JalaliDateTime(JalaliDate):
|
|
|
1631
1751
|
)
|
|
1632
1752
|
|
|
1633
1753
|
data_string_regex = utils.replace(fmt, directives_regex_pattern)
|
|
1754
|
+
full_pattern = f"^{data_string_regex}$"
|
|
1634
1755
|
|
|
1635
|
-
if re.match(
|
|
1756
|
+
if re.match(full_pattern, data_string, re.IGNORECASE):
|
|
1636
1757
|
directives = re.search(data_string_regex, data_string, re.IGNORECASE).groupdict()
|
|
1637
1758
|
|
|
1638
1759
|
if "Y" in directives.keys() and len(directives.get("Y")) < 4:
|
|
@@ -1684,17 +1805,6 @@ class JalaliDateTime(JalaliDate):
|
|
|
1684
1805
|
else:
|
|
1685
1806
|
raise ValueError("data string and format are not matched")
|
|
1686
1807
|
|
|
1687
|
-
def __seqToRE(self, to_convert, directive):
|
|
1688
|
-
to_convert = sorted(to_convert, key=len, reverse=True)
|
|
1689
|
-
for value in to_convert:
|
|
1690
|
-
if value != "":
|
|
1691
|
-
break
|
|
1692
|
-
else:
|
|
1693
|
-
return ""
|
|
1694
|
-
regex = "|".join(re_escape(stuff) for stuff in to_convert)
|
|
1695
|
-
regex = f"(?P<{directive}>{regex}"
|
|
1696
|
-
return "%s)" % regex
|
|
1697
|
-
|
|
1698
1808
|
def __repr__(self):
|
|
1699
1809
|
"""Convert to formal string, for repr()."""
|
|
1700
1810
|
d_datetime = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: persiantools
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.0
|
|
4
4
|
Summary: Jalali date and datetime with other tools
|
|
5
5
|
Home-page: https://github.com/majiidd/persiantools
|
|
6
6
|
Author: Majid Hajiloo
|
|
@@ -8,7 +8,6 @@ Author-email: majid.hajiloo@gmail.com
|
|
|
8
8
|
License: MIT
|
|
9
9
|
Keywords: jalali shamsi persian digits characters converter jalalidate jalalidatetime date datetime jdate jdatetime farsi
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
11
|
Classifier: Natural Language :: Persian
|
|
13
12
|
Classifier: Operating System :: OS Independent
|
|
14
13
|
Classifier: Programming Language :: Python
|
|
@@ -18,7 +18,6 @@ setup(
|
|
|
18
18
|
long_description_content_type="text/markdown",
|
|
19
19
|
classifiers=[
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
|
-
"License :: OSI Approved :: MIT License",
|
|
22
21
|
"Natural Language :: Persian",
|
|
23
22
|
"Operating System :: OS Independent",
|
|
24
23
|
"Programming Language :: Python",
|
|
@@ -90,3 +90,14 @@ class TestDigits(TestCase):
|
|
|
90
90
|
|
|
91
91
|
with pytest.raises(TypeError):
|
|
92
92
|
digits.to_word("123")
|
|
93
|
+
|
|
94
|
+
def test_fractional_part_too_long(self):
|
|
95
|
+
old_mantissa = digits.MANTISSA
|
|
96
|
+
try:
|
|
97
|
+
digits.MANTISSA = ("دهم",)
|
|
98
|
+
with self.assertRaises(ValueError, msg="Fractional part is too long"):
|
|
99
|
+
# 0.15 -> left = "0", right = "15", right.rstrip("0") == "15"
|
|
100
|
+
# mantissa_index = len("15") - 1 == 1, which is >= len(MANTISSA) == 1.
|
|
101
|
+
digits.to_word(0.15)
|
|
102
|
+
finally:
|
|
103
|
+
digits.MANTISSA = old_mantissa
|
|
@@ -13,6 +13,7 @@ class TestJalaliDate(TestCase):
|
|
|
13
13
|
def test_shamsi_to_gregorian(self):
|
|
14
14
|
cases = [
|
|
15
15
|
(JalaliDate(1100, 1, 1), date(1721, 3, 21)),
|
|
16
|
+
(JalaliDate(1210, 12, 30), date(1832, 3, 20)),
|
|
16
17
|
(JalaliDate(1367, 2, 14), date(1988, 5, 4)),
|
|
17
18
|
(JalaliDate(1395, 3, 21), date(2016, 6, 10)),
|
|
18
19
|
(JalaliDate(1395, 12, 9), date(2017, 2, 27)),
|
|
@@ -67,6 +68,7 @@ class TestJalaliDate(TestCase):
|
|
|
67
68
|
(JalaliDate(1402, 1, 1), date(2023, 3, 21)),
|
|
68
69
|
(JalaliDate(1403, 1, 1), date(2024, 3, 20)),
|
|
69
70
|
(JalaliDate(1404, 1, 1), date(2025, 3, 21)),
|
|
71
|
+
(JalaliDate(1498, 12, 30), date(2120, 3, 20)),
|
|
70
72
|
(JalaliDate(1505, 1, 1), date(2126, 3, 21)),
|
|
71
73
|
(JalaliDate.today(), date.today()),
|
|
72
74
|
]
|
|
@@ -124,6 +126,9 @@ class TestJalaliDate(TestCase):
|
|
|
124
126
|
|
|
125
127
|
def test_checkdate(self):
|
|
126
128
|
cases = [
|
|
129
|
+
(1206, 12, 30, False),
|
|
130
|
+
(1210, 12, 30, True),
|
|
131
|
+
(1214, 12, 30, True),
|
|
127
132
|
(1367, 2, 14, True),
|
|
128
133
|
(1395, 12, 30, True),
|
|
129
134
|
(1394, 12, 30, False),
|
|
@@ -136,14 +141,21 @@ class TestJalaliDate(TestCase):
|
|
|
136
141
|
(1396, 7, 27, True),
|
|
137
142
|
(1397, 11, 29, True),
|
|
138
143
|
(1399, 11, 31, False),
|
|
144
|
+
(1400, 1, 32, False),
|
|
139
145
|
(1400, 4, 25, True),
|
|
140
146
|
(1400, 12, 30, False),
|
|
141
147
|
(1403, 4, 3, True),
|
|
142
148
|
(1403, 12, 30, True),
|
|
149
|
+
(1473, 12, 30, False),
|
|
150
|
+
(1474, 12, 30, True),
|
|
151
|
+
(1498, 12, 30, True),
|
|
143
152
|
]
|
|
144
153
|
for year, month, day, valid in cases:
|
|
145
154
|
self.assertEqual(JalaliDate.check_date(year, month, day), valid)
|
|
146
155
|
|
|
156
|
+
with pytest.raises(ValueError):
|
|
157
|
+
JalaliDate._check_date_fields(1404, 3, 16, "ar")
|
|
158
|
+
|
|
147
159
|
def test_completeday(self):
|
|
148
160
|
jdate = JalaliDate(1398, 3, 17)
|
|
149
161
|
self.assertEqual(jdate.year, 1398)
|
|
@@ -211,6 +223,18 @@ class TestJalaliDate(TestCase):
|
|
|
211
223
|
with pytest.raises(ValueError, match="locale must be 'en' or 'fa'"):
|
|
212
224
|
jdate.replace(locale="de")
|
|
213
225
|
|
|
226
|
+
with pytest.raises(ValueError):
|
|
227
|
+
JalaliDate.days_before_month(0)
|
|
228
|
+
|
|
229
|
+
with pytest.raises(ValueError):
|
|
230
|
+
JalaliDate.days_before_month(13)
|
|
231
|
+
|
|
232
|
+
with pytest.raises(ValueError):
|
|
233
|
+
JalaliDate.days_in_month(13, 1404)
|
|
234
|
+
|
|
235
|
+
with pytest.raises(ValueError):
|
|
236
|
+
JalaliDate.days_in_month(0, 1400)
|
|
237
|
+
|
|
214
238
|
def test_leap(self):
|
|
215
239
|
cases = [
|
|
216
240
|
# First 33-year cycle
|
|
@@ -409,6 +433,24 @@ class TestJalaliDate(TestCase):
|
|
|
409
433
|
with pytest.raises(ValueError):
|
|
410
434
|
JalaliDate.fromisoformat("2021W123")
|
|
411
435
|
|
|
436
|
+
with pytest.raises(ValueError):
|
|
437
|
+
JalaliDate.fromisoformat("1395-03")
|
|
438
|
+
|
|
439
|
+
with pytest.raises(ValueError):
|
|
440
|
+
JalaliDate.fromisoformat("13950301")
|
|
441
|
+
|
|
442
|
+
with pytest.raises(ValueError):
|
|
443
|
+
JalaliDate.fromisoformat("1395-13-01")
|
|
444
|
+
|
|
445
|
+
with pytest.raises(ValueError):
|
|
446
|
+
JalaliDate.fromisoformat("1400-01-32")
|
|
447
|
+
|
|
448
|
+
with pytest.raises(ValueError):
|
|
449
|
+
JalaliDate.fromisoformat("1395-03-01 extra")
|
|
450
|
+
|
|
451
|
+
with pytest.raises(ValueError):
|
|
452
|
+
JalaliDate.fromisoformat(" 1395-03-01")
|
|
453
|
+
|
|
412
454
|
j_date = JalaliDate(1400, 1, 1)
|
|
413
455
|
self.assertEqual(j_date.strftime("%A, %d %B %Y"), "Yekshanbeh, 01 Farvardin 1400")
|
|
414
456
|
self.assertEqual(j_date.strftime("%A, %d %B %Y", locale="fa"), "یکشنبه, ۰۱ فروردین ۱۴۰۰")
|
|
@@ -561,8 +603,145 @@ class TestJalaliDate(TestCase):
|
|
|
561
603
|
self.assertEqual(repr(JalaliDate(1403, 4, 7)), "JalaliDate(1403, 4, 7, Panjshanbeh)")
|
|
562
604
|
|
|
563
605
|
def test_strptime(self):
|
|
564
|
-
|
|
565
|
-
|
|
606
|
+
# 1. Basic parsing
|
|
607
|
+
self.assertEqual(JalaliDate.strptime("1367-02-14", "%Y-%m-%d"), JalaliDate(1367, 2, 14))
|
|
608
|
+
self.assertEqual(JalaliDate.strptime("1367/02/14", "%Y/%m/%d"), JalaliDate(1367, 2, 14))
|
|
609
|
+
self.assertEqual(JalaliDate.strptime("13670214", "%Y%m%d"), JalaliDate(1367, 2, 14))
|
|
610
|
+
self.assertEqual(JalaliDate.strptime("16/03/1404", "%d/%m/%Y"), JalaliDate(1404, 3, 16))
|
|
611
|
+
self.assertEqual(JalaliDate.strptime("16/03/1404", "%d/%m/%Y", locale="en"), JalaliDate(1404, 3, 16))
|
|
612
|
+
|
|
613
|
+
# 2. Year variations
|
|
614
|
+
self.assertEqual(JalaliDate.strptime("99-01-01", "%y-%m-%d"), JalaliDate(1399, 1, 1)) # 99 > 70 => 1399
|
|
615
|
+
self.assertEqual(JalaliDate.strptime("00-01-01", "%y-%m-%d"), JalaliDate(1400, 1, 1))
|
|
616
|
+
self.assertEqual(JalaliDate.strptime("01-01-01", "%y-%m-%d"), JalaliDate(1401, 1, 1)) # 01 <= 70 => 1401
|
|
617
|
+
self.assertEqual(JalaliDate.strptime("04-01-01", "%y-%m-%d"), JalaliDate(1404, 1, 1))
|
|
618
|
+
self.assertEqual(JalaliDate.strptime("70-10-10", "%y-%m-%d"), JalaliDate(1470, 10, 10)) # 70 => 1470
|
|
619
|
+
|
|
620
|
+
# 3. Month variations
|
|
621
|
+
self.assertEqual(JalaliDate.strptime("1400-01-15", "%Y-%m-%d"), JalaliDate(1400, 1, 15))
|
|
622
|
+
self.assertEqual(JalaliDate.strptime("1400-Far-15", "%Y-%b-%d"), JalaliDate(1400, 1, 15))
|
|
623
|
+
self.assertEqual(JalaliDate.strptime("1400-FAR-15", "%Y-%b-%d"), JalaliDate(1400, 1, 15))
|
|
624
|
+
self.assertEqual(JalaliDate.strptime("1400-far-15", "%Y-%b-%d"), JalaliDate(1400, 1, 15))
|
|
625
|
+
self.assertEqual(JalaliDate.strptime("1367-Ord-14", "%Y-%b-%d"), JalaliDate(1367, 2, 14))
|
|
626
|
+
self.assertEqual(JalaliDate.strptime("1400-Farvardin-15", "%Y-%B-%d"), JalaliDate(1400, 1, 15))
|
|
627
|
+
self.assertEqual(JalaliDate.strptime("1400-FARVARDIN-15", "%Y-%B-%d"), JalaliDate(1400, 1, 15))
|
|
628
|
+
self.assertEqual(JalaliDate.strptime("1400-farvardin-15", "%Y-%B-%d"), JalaliDate(1400, 1, 15))
|
|
629
|
+
self.assertEqual(JalaliDate.strptime("1400-Ordibehesht-10", "%Y-%B-%d"), JalaliDate(1400, 2, 10))
|
|
630
|
+
self.assertEqual(JalaliDate.strptime("1398-Esf-05", "%Y-%b-%d"), JalaliDate(1398, 12, 5))
|
|
631
|
+
self.assertEqual(JalaliDate.strptime("1403-Esfand-30", "%Y-%B-%d"), JalaliDate(1403, 12, 30))
|
|
632
|
+
|
|
633
|
+
# 4. Day variations
|
|
634
|
+
self.assertEqual(JalaliDate.strptime("1404-03-05", "%Y-%m-%d"), JalaliDate(1404, 3, 5))
|
|
635
|
+
self.assertEqual(JalaliDate.strptime("1404-03-31", "%Y-%m-%d"), JalaliDate(1404, 3, 31))
|
|
636
|
+
|
|
637
|
+
# 5. Locale testing ('en' and 'fa')
|
|
638
|
+
self.assertEqual(
|
|
639
|
+
JalaliDate.strptime("1401-Ord-05", "%Y-%b-%d", locale="en"), JalaliDate(1401, 2, 5, locale="en")
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
# Persian locale
|
|
643
|
+
self.assertEqual(
|
|
644
|
+
JalaliDate.strptime("۱۴۰۰-۰۱-۱۵", "%Y-%m-%d", locale="fa"), JalaliDate(1400, 1, 15, locale="fa")
|
|
645
|
+
)
|
|
646
|
+
self.assertEqual(
|
|
647
|
+
JalaliDate.strptime("۱۴۰۰-فروردین-۱۵", "%Y-%B-%d", locale="fa"), JalaliDate(1400, 1, 15, locale="fa")
|
|
648
|
+
)
|
|
649
|
+
self.assertEqual(
|
|
650
|
+
JalaliDate.strptime("۱۴۰۰-فرو-۱۵", "%Y-%b-%d", locale="fa"), JalaliDate(1400, 1, 15, locale="fa")
|
|
651
|
+
)
|
|
652
|
+
self.assertEqual(
|
|
653
|
+
JalaliDate.strptime("۱۴۰۰-اردیبهشت-۱۰", "%Y-%B-%d", locale="fa"), JalaliDate(1400, 2, 10, locale="fa")
|
|
654
|
+
)
|
|
655
|
+
self.assertEqual(
|
|
656
|
+
JalaliDate.strptime("۱۳۹۸-اسفند-۰۵", "%Y-%B-%d", locale="fa"), JalaliDate(1398, 12, 5, locale="fa")
|
|
657
|
+
)
|
|
658
|
+
self.assertEqual(
|
|
659
|
+
JalaliDate.strptime("۱۳۹۸-اسف-۰۵", "%Y-%b-%d", locale="fa"), JalaliDate(1398, 12, 5, locale="fa")
|
|
660
|
+
)
|
|
661
|
+
# Mixed Farsi digits and names
|
|
662
|
+
self.assertEqual(
|
|
663
|
+
JalaliDate.strptime("1398-اسفند-۰۵", "%Y-%B-%d", locale="fa"), JalaliDate(1398, 12, 5, locale="fa")
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# 6. Format strings with literals and different separators
|
|
667
|
+
self.assertEqual(JalaliDate.strptime("Date: 1404/12 Day: 29", "Date: %Y/%m Day: %d"), JalaliDate(1404, 12, 29))
|
|
668
|
+
self.assertEqual(
|
|
669
|
+
JalaliDate.strptime("Year 1367 Month 02 Day 14", "Year %Y Month %m Day %d"), JalaliDate(1367, 2, 14)
|
|
670
|
+
)
|
|
671
|
+
# self.assertEqual(JalaliDate.strptime("1400|01|01", "%Y|%m|%d"), JalaliDate(1400, 1, 1))
|
|
672
|
+
self.assertEqual(JalaliDate.strptime("Jalali:1403-10-20", "Jalali:%Y-%m-%d"), JalaliDate(1403, 10, 20))
|
|
673
|
+
|
|
674
|
+
# 7. Weekday directives (%a and %A) - parsed but ignored for date construction
|
|
675
|
+
self.assertEqual(
|
|
676
|
+
JalaliDate.strptime("Jomeh, 1404-03-16", "%A, %Y-%m-%d"), JalaliDate(1404, 3, 16)
|
|
677
|
+
) # 1404-03-16 is a Jomeh
|
|
678
|
+
self.assertEqual(JalaliDate.strptime("Sha, 1400-01-06", "%a, %Y-%m-%d"), JalaliDate(1400, 1, 6))
|
|
679
|
+
self.assertEqual(JalaliDate.strptime("Yekshanbeh 1400-Farvardin-01", "%A %Y-%B-%d"), JalaliDate(1400, 1, 1))
|
|
680
|
+
# Test with fa locale weekdays
|
|
681
|
+
self.assertEqual(
|
|
682
|
+
JalaliDate.strptime("شنبه, ۱۴۰۰-۰۱-۰۶", "%A, %Y-%m-%d", locale="fa"), JalaliDate(1400, 1, 6, locale="fa")
|
|
683
|
+
)
|
|
684
|
+
self.assertEqual(
|
|
685
|
+
JalaliDate.strptime("ش, ۱۴۰۰-۰۱-۰۶", "%a, %Y-%m-%d", locale="fa"), JalaliDate(1400, 1, 6, locale="fa")
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
# 8. Invalid inputs
|
|
689
|
+
with self.assertRaises(ValueError, msg="Month out of range"):
|
|
690
|
+
JalaliDate.strptime("1400-13-01", "%Y-%m-%d")
|
|
691
|
+
with self.assertRaises(ValueError, msg="Day out of range for month"):
|
|
692
|
+
JalaliDate.strptime("1400-01-32", "%Y-%m-%d")
|
|
693
|
+
with self.assertRaises(ValueError, msg="Day out of range for month (Mehr has 30 days)"):
|
|
694
|
+
JalaliDate.strptime("1400-07-31", "%Y-%m-%d")
|
|
695
|
+
with self.assertRaises(ValueError, msg="Invalid month name"):
|
|
696
|
+
JalaliDate.strptime("1399/Feb/20", "%Y/%b/%d")
|
|
697
|
+
with self.assertRaises(ValueError, msg="String does not match format"):
|
|
698
|
+
JalaliDate.strptime("1400/01/01 Extra", "%Y/%m/%d")
|
|
699
|
+
with self.assertRaises(ValueError, msg="String does not match format - incorrect separator"):
|
|
700
|
+
JalaliDate.strptime("1400.01.01", "%Y-%m-%d")
|
|
701
|
+
with self.assertRaises(ValueError, msg="Year with %Y must be 4 digits"):
|
|
702
|
+
JalaliDate.strptime("123-01-01", "%Y-%m-%d")
|
|
703
|
+
with self.assertRaises(ValueError, msg="Year with %y must be 2 digits"): # The regex for %y is \d{2}
|
|
704
|
+
JalaliDate.strptime("1-01-01", "%y-%m-%d")
|
|
705
|
+
with self.assertRaises(ValueError, msg="Date string does not match format (empty date string)"):
|
|
706
|
+
JalaliDate.strptime("", "%Y-%m-%d")
|
|
707
|
+
with self.assertRaises(ValueError, msg="Date string does not match format (empty format string)"):
|
|
708
|
+
JalaliDate.strptime("1400-01-01", "")
|
|
709
|
+
with self.assertRaises(ValueError, msg="Month information missing"):
|
|
710
|
+
JalaliDate.strptime("1400--01", "%Y-%m-%d")
|
|
711
|
+
with self.assertRaises(ValueError, msg="Day information missing"):
|
|
712
|
+
JalaliDate.strptime("1400-01-", "%Y-%m-%d")
|
|
713
|
+
with self.assertRaises(ValueError, msg="Year information missing"):
|
|
714
|
+
JalaliDate.strptime("-01-01", "%Y-%m-%d")
|
|
715
|
+
with self.assertRaises(ValueError):
|
|
716
|
+
JalaliDate.strptime("1402-12-30", "%Y-%m-%d")
|
|
717
|
+
with self.assertRaises(ValueError):
|
|
718
|
+
JalaliDate.strptime("98-12-30", "%y-%m-%d")
|
|
719
|
+
with self.assertRaises(ValueError):
|
|
720
|
+
JalaliDate.strptime("1404-01-01", "%Y-%m-%d", "ar")
|
|
721
|
+
|
|
722
|
+
# 9. Edge cases
|
|
723
|
+
# Leap year: 1399 was a leap year (Esfand has 30 days)
|
|
724
|
+
self.assertEqual(JalaliDate.strptime("1399-12-30", "%Y-%m-%d"), JalaliDate(1399, 12, 30))
|
|
725
|
+
# Non-leap year: 1400 was not a leap year (Esfand has 29 days)
|
|
726
|
+
self.assertEqual(JalaliDate.strptime("1400-12-29", "%Y-%m-%d"), JalaliDate(1400, 12, 29))
|
|
727
|
+
with self.assertRaises(ValueError, msg="Esfand 30 on non-leap year 1400"):
|
|
728
|
+
JalaliDate.strptime("1400-12-30", "%Y-%m-%d")
|
|
729
|
+
|
|
730
|
+
# Leap year: 1403 is a leap year
|
|
731
|
+
self.assertEqual(JalaliDate.strptime("1403-12-30", "%Y-%m-%d"), JalaliDate(1403, 12, 30))
|
|
732
|
+
self.assertEqual(JalaliDate.strptime("03-12-30", "%y-%m-%d"), JalaliDate(1403, 12, 30))
|
|
733
|
+
|
|
734
|
+
self.assertEqual(JalaliDate.strptime(f"{MINYEAR:04d}-01-01", "%Y-%m-%d"), JalaliDate(MINYEAR, 1, 1))
|
|
735
|
+
self.assertEqual(JalaliDate.strptime(f"{MAXYEAR:04d}-12-29", "%Y-%m-%d"), JalaliDate(MAXYEAR, 12, 29))
|
|
736
|
+
|
|
737
|
+
# Test %x and %c replacements
|
|
738
|
+
# %x: %Y/%m/%d
|
|
739
|
+
self.assertEqual(JalaliDate.strptime("1399/05/10", "%x"), JalaliDate(1399, 5, 10))
|
|
740
|
+
# %c: %a %b %d %Y
|
|
741
|
+
self.assertEqual(JalaliDate.strptime("Sha Far 01 1380", "%c"), JalaliDate(1380, 1, 1)) # 1380-01-01 is Shanbeh
|
|
742
|
+
self.assertEqual(JalaliDate.strptime("ش فرو ۰۱ ۱۳۸۰", "%c", locale="fa"), JalaliDate(1380, 1, 1, locale="fa"))
|
|
743
|
+
|
|
744
|
+
self.assertEqual(JalaliDate.strptime("1400-Tir-15", "%Y-%b-%d"), JalaliDate(1400, 4, 15))
|
|
566
745
|
|
|
567
746
|
def test_locale_setter_invalid_value(self):
|
|
568
747
|
jdate = JalaliDate.today()
|
|
@@ -92,6 +92,23 @@ class TestJalaliDateTime(TestCase):
|
|
|
92
92
|
time.struct_time((1988, 5, 4, 14, 0, 0, 2, 125, 0)),
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
+
def test_check_utc_offset(self):
|
|
96
|
+
JalaliDateTime.check_utc_offset("utcoffset", None)
|
|
97
|
+
JalaliDateTime.check_utc_offset("dst", None)
|
|
98
|
+
|
|
99
|
+
offset = timedelta(minutes=30)
|
|
100
|
+
JalaliDateTime.check_utc_offset("utcoffset", offset)
|
|
101
|
+
JalaliDateTime.check_utc_offset("dst", offset)
|
|
102
|
+
|
|
103
|
+
with pytest.raises(AssertionError):
|
|
104
|
+
JalaliDateTime.check_utc_offset("invalid", timedelta(minutes=30))
|
|
105
|
+
|
|
106
|
+
with pytest.raises(TypeError):
|
|
107
|
+
JalaliDateTime.check_utc_offset("utcoffset", 30)
|
|
108
|
+
|
|
109
|
+
with pytest.raises(ValueError):
|
|
110
|
+
JalaliDateTime.check_utc_offset("dst", timedelta(seconds=61))
|
|
111
|
+
|
|
95
112
|
def test_others(self):
|
|
96
113
|
self.assertTrue(JalaliDateTime.fromtimestamp(time.time() - 10) <= JalaliDateTime.now())
|
|
97
114
|
self.assertEqual(JalaliDateTime(1367, 2, 14, 4, 30, 0, 0, pytz.utc).timestamp(), 578723400)
|
|
@@ -114,6 +131,9 @@ class TestJalaliDateTime(TestCase):
|
|
|
114
131
|
self.assertIsNone(JalaliDateTime.today().tzname())
|
|
115
132
|
self.assertIsNone(JalaliDateTime.today().dst())
|
|
116
133
|
|
|
134
|
+
dt = JalaliDateTime(1367, 2, 14, 4, 30, 0, 0, pytz.utc)
|
|
135
|
+
self.assertEqual(dt.dst(), timedelta(0))
|
|
136
|
+
|
|
117
137
|
self.assertEqual(
|
|
118
138
|
JalaliDateTime(1367, 2, 14, 4, 30, 0, 0).ctime(),
|
|
119
139
|
"Chaharshanbeh 14 Ordibehesht 1367 04:30:00",
|
|
@@ -329,6 +349,11 @@ class TestJalaliDateTime(TestCase):
|
|
|
329
349
|
jdt = JalaliDateTime(1374, 4, 8, 16, 28, 3, 227, pytz.utc)
|
|
330
350
|
self.assertEqual(jdt, JalaliDateTime.strptime(jdt.strftime("%c %f %z %Z"), "%c %f %z %Z"))
|
|
331
351
|
|
|
352
|
+
jdt_utc_combined = JalaliDateTime(1374, 4, 8, 16, 28, 3, 227, timezone.utc)
|
|
353
|
+
self.assertEqual(
|
|
354
|
+
jdt_utc_combined, JalaliDateTime.strptime(jdt_utc_combined.strftime("%c %f %z %Z"), "%c %f %z %Z")
|
|
355
|
+
)
|
|
356
|
+
|
|
332
357
|
def test_strptime_basic(self):
|
|
333
358
|
date_string = "1400-01-01 12:30:45"
|
|
334
359
|
fmt = "%Y-%m-%d %H:%M:%S"
|
|
@@ -352,6 +377,98 @@ class TestJalaliDateTime(TestCase):
|
|
|
352
377
|
self.assertEqual(jdt.second, 45)
|
|
353
378
|
self.assertEqual(jdt.utcoffset(), timedelta(hours=3, minutes=30))
|
|
354
379
|
|
|
380
|
+
def test_strptime_z_directive_valid_formats(self):
|
|
381
|
+
base_dt_str = "1400-01-01 10:00:00"
|
|
382
|
+
fmt_base = "%Y-%m-%d %H:%M:%S"
|
|
383
|
+
|
|
384
|
+
valid_z_formats = [
|
|
385
|
+
("+0330", 3, 30),
|
|
386
|
+
("-0500", -5, 0),
|
|
387
|
+
("+0430", 4, 30),
|
|
388
|
+
("-0715", -7, -15),
|
|
389
|
+
("+0000", 0, 0),
|
|
390
|
+
("-0000", 0, 0),
|
|
391
|
+
("+1400", 14, 0),
|
|
392
|
+
("-1400", -14, 0),
|
|
393
|
+
("+0059", 0, 59),
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
for tz_str, hours, minutes in valid_z_formats:
|
|
397
|
+
date_string = f"{base_dt_str} {tz_str}"
|
|
398
|
+
fmt = f"{fmt_base} %z"
|
|
399
|
+
with self.subTest(date_string=date_string, fmt=fmt):
|
|
400
|
+
jdt = JalaliDateTime.strptime(date_string, fmt)
|
|
401
|
+
self.assertIsNotNone(jdt.tzinfo, f"tzinfo should not be None for {date_string}")
|
|
402
|
+
self.assertIsInstance(jdt.tzinfo, timezone, f"tzinfo should be datetime.timezone for {date_string}")
|
|
403
|
+
expected_offset = timedelta(hours=hours, minutes=minutes)
|
|
404
|
+
self.assertEqual(jdt.utcoffset(), expected_offset, f"Offset mismatch for {date_string}")
|
|
405
|
+
self.assertEqual(jdt.year, 1400)
|
|
406
|
+
self.assertEqual(jdt.hour, 10)
|
|
407
|
+
|
|
408
|
+
def test_strptime_z_directive_invalid_formats(self):
|
|
409
|
+
base_dt_str = "1400-01-01 10:00:00"
|
|
410
|
+
fmt_base = "%Y-%m-%d %H:%M:%S"
|
|
411
|
+
|
|
412
|
+
invalid_z_formats = [
|
|
413
|
+
"0330", # Missing sign: In Python, the %z directive requires a leading '+' or '-' for the timezone offset.
|
|
414
|
+
"+330", # Incorrect number of digits (hour)
|
|
415
|
+
"+033", # Incorrect number of digits (minute)
|
|
416
|
+
"+03:3", # Incorrect number of digits (minute with colon)
|
|
417
|
+
"+03-30", # Invalid separator
|
|
418
|
+
"03:30", # Missing sign with colon
|
|
419
|
+
"+03:300", # Too many minute digits
|
|
420
|
+
"+030:30", # Too many hour digits
|
|
421
|
+
"+0A30", # Non-numeric hour
|
|
422
|
+
"+03B0", # Non-numeric minute
|
|
423
|
+
"+2500", # Hour out of range (max is +2359 or +1400 for this specific regex in Python for %z)
|
|
424
|
+
"+2400", # Hour part out of range (>=24)
|
|
425
|
+
"-2400",
|
|
426
|
+
"+0360", # Minute part out of range (>=60)
|
|
427
|
+
"-0360",
|
|
428
|
+
"+03:60",
|
|
429
|
+
" +0330", # Leading space before offset
|
|
430
|
+
"+0330 ", # Trailing space after offset (will cause full string match failure)
|
|
431
|
+
"GMT+0330", # Non-standard prefix
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
for tz_str in invalid_z_formats:
|
|
435
|
+
date_string = f"{base_dt_str} {tz_str}"
|
|
436
|
+
fmt = f"{fmt_base} %z"
|
|
437
|
+
with self.subTest(date_string=date_string, fmt=fmt):
|
|
438
|
+
with self.assertRaises(ValueError, msg=f"Failed for invalid tz string: {tz_str}"):
|
|
439
|
+
JalaliDateTime.strptime(date_string, fmt)
|
|
440
|
+
|
|
441
|
+
# Test %z without enough characters following
|
|
442
|
+
with self.assertRaises(ValueError):
|
|
443
|
+
JalaliDateTime.strptime(f"{base_dt_str} +", f"{fmt_base} %z")
|
|
444
|
+
with self.assertRaises(ValueError):
|
|
445
|
+
JalaliDateTime.strptime(f"{base_dt_str} +03", f"{fmt_base} %z")
|
|
446
|
+
|
|
447
|
+
def test_strptime_z_directive_locale_independence(self):
|
|
448
|
+
base_dt_str = "1400-01-01 10:00:00"
|
|
449
|
+
tz_str = "+0545"
|
|
450
|
+
fmt = "%Y-%m-%d %H:%M:%S %z"
|
|
451
|
+
date_string = f"{base_dt_str} {tz_str}"
|
|
452
|
+
expected_offset = timedelta(hours=5, minutes=45)
|
|
453
|
+
|
|
454
|
+
# Test with locale 'en'
|
|
455
|
+
jdt_en = JalaliDateTime.strptime(date_string, fmt, locale="en")
|
|
456
|
+
self.assertIsNotNone(jdt_en.tzinfo)
|
|
457
|
+
self.assertEqual(jdt_en.utcoffset(), expected_offset)
|
|
458
|
+
self.assertEqual(jdt_en.year, 1400)
|
|
459
|
+
|
|
460
|
+
# Test with locale 'fa'
|
|
461
|
+
jdt_fa = JalaliDateTime.strptime(date_string, fmt, locale="fa")
|
|
462
|
+
self.assertIsNotNone(jdt_fa.tzinfo)
|
|
463
|
+
self.assertEqual(jdt_fa.utcoffset(), expected_offset)
|
|
464
|
+
self.assertEqual(jdt_fa.year, 1400)
|
|
465
|
+
|
|
466
|
+
date_string_fa_main = f"۱۴۰۰-۰۱-۰۱ ۱۰:۰۰:۰۰ {tz_str}"
|
|
467
|
+
jdt_fa_main = JalaliDateTime.strptime(date_string_fa_main, fmt, locale="fa")
|
|
468
|
+
self.assertIsNotNone(jdt_fa_main.tzinfo)
|
|
469
|
+
self.assertEqual(jdt_fa_main.utcoffset(), expected_offset)
|
|
470
|
+
self.assertEqual(jdt_fa_main.year, 1400)
|
|
471
|
+
|
|
355
472
|
def test_strptime_with_locale_fa(self):
|
|
356
473
|
date_string = "۱۴۰۰-۰۱-۰۱ ۱۲:۳۰:۴۵"
|
|
357
474
|
fmt = "%Y-%m-%d %H:%M:%S"
|
|
@@ -612,6 +729,11 @@ class TestJalaliDateTime(TestCase):
|
|
|
612
729
|
with self.assertRaises(ValueError):
|
|
613
730
|
JalaliDateTime.fromisoformat("invalid-date-time")
|
|
614
731
|
|
|
732
|
+
self.assertEqual(JalaliDateTime._find_isoformat_datetime_separator("2021W12"), 7)
|
|
733
|
+
|
|
734
|
+
with pytest.raises(ValueError, match="Invalid ISO string"):
|
|
735
|
+
JalaliDateTime._find_isoformat_datetime_separator("2021-W12-")
|
|
736
|
+
|
|
615
737
|
def test_find_isoformat_datetime_separator(self):
|
|
616
738
|
separator = JalaliDateTime._find_isoformat_datetime_separator("1403-08-09T02:21:45")
|
|
617
739
|
self.assertEqual(separator, 10)
|
|
@@ -637,3 +759,66 @@ class TestJalaliDateTime(TestCase):
|
|
|
637
759
|
iso_format = original.isoformat()
|
|
638
760
|
parsed = JalaliDateTime.fromisoformat(iso_format)
|
|
639
761
|
self.assertEqual(original, parsed)
|
|
762
|
+
|
|
763
|
+
def test_isoformat_timezone_representation(self):
|
|
764
|
+
# 1. UTC Timezone
|
|
765
|
+
jdt_utc = JalaliDateTime(1400, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
766
|
+
self.assertEqual(jdt_utc.isoformat(), "1400-01-01T12:00:00+00:00")
|
|
767
|
+
|
|
768
|
+
# 2. Positive Offset Timezones
|
|
769
|
+
tz_plus_3_30 = timezone(timedelta(hours=3, minutes=30))
|
|
770
|
+
jdt_plus_3_30 = JalaliDateTime(1401, 2, 15, 10, 30, 0, tzinfo=tz_plus_3_30)
|
|
771
|
+
self.assertEqual(jdt_plus_3_30.isoformat(), "1401-02-15T10:30:00+03:30")
|
|
772
|
+
|
|
773
|
+
tz_plus_5 = timezone(timedelta(hours=5))
|
|
774
|
+
jdt_plus_5 = JalaliDateTime(1402, 3, 20, 8, 0, 0, tzinfo=tz_plus_5)
|
|
775
|
+
self.assertEqual(jdt_plus_5.isoformat(), "1402-03-20T08:00:00+05:00")
|
|
776
|
+
|
|
777
|
+
# 3. Negative Offset Timezones
|
|
778
|
+
tz_minus_4_45 = timezone(timedelta(hours=-4, minutes=-45))
|
|
779
|
+
jdt_minus_4_45 = JalaliDateTime(1403, 4, 10, 16, 15, 0, tzinfo=tz_minus_4_45)
|
|
780
|
+
self.assertEqual(jdt_minus_4_45.isoformat(), "1403-04-10T16:15:00-04:45")
|
|
781
|
+
|
|
782
|
+
tz_minus_4_45 = timezone(timedelta(hours=-4, minutes=45))
|
|
783
|
+
jdt_minus_4_45 = JalaliDateTime(1403, 4, 10, 16, 15, 0, tzinfo=tz_minus_4_45)
|
|
784
|
+
self.assertEqual(jdt_minus_4_45.isoformat(), "1403-04-10T16:15:00-03:15")
|
|
785
|
+
|
|
786
|
+
tz_direct_minus_4_45 = timezone(timedelta(seconds=-(4 * 3600 + 45 * 60)))
|
|
787
|
+
jdt_direct_minus_4_45 = JalaliDateTime(1403, 4, 10, 16, 15, 0, tzinfo=tz_direct_minus_4_45)
|
|
788
|
+
self.assertEqual(jdt_direct_minus_4_45.isoformat(), "1403-04-10T16:15:00-04:45")
|
|
789
|
+
|
|
790
|
+
tz_minus_8 = timezone(timedelta(hours=-8))
|
|
791
|
+
jdt_minus_8 = JalaliDateTime(1399, 12, 29, 23, 30, 59, tzinfo=tz_minus_8)
|
|
792
|
+
self.assertEqual(jdt_minus_8.isoformat(), "1399-12-29T23:30:59-08:00")
|
|
793
|
+
|
|
794
|
+
# 4. Offsets with zero minutes (covered by +05:00 and -08:00 above)
|
|
795
|
+
self.assertEqual(
|
|
796
|
+
JalaliDateTime(1402, 3, 20, 8, 0, 0, tzinfo=timezone(timedelta(hours=5))).isoformat(),
|
|
797
|
+
"1402-03-20T08:00:00+05:00",
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# 5. Microseconds with timezone offset
|
|
801
|
+
jdt_utc_ms = JalaliDateTime(1400, 1, 1, 12, 30, 45, 123456, tzinfo=timezone.utc)
|
|
802
|
+
self.assertEqual(jdt_utc_ms.isoformat(), "1400-01-01T12:30:45.123456+00:00")
|
|
803
|
+
|
|
804
|
+
jdt_offset_ms = JalaliDateTime(1400, 6, 10, 10, 20, 30, 654321, tzinfo=tz_plus_3_30)
|
|
805
|
+
self.assertEqual(jdt_offset_ms.isoformat(), "1400-06-10T10:20:30.654321+03:30")
|
|
806
|
+
|
|
807
|
+
# 6. Default Separator 'T' (implicitly tested in all above cases)
|
|
808
|
+
self.assertIn("T", jdt_utc.isoformat())
|
|
809
|
+
|
|
810
|
+
# 7. Custom Separator
|
|
811
|
+
jdt_custom_sep = JalaliDateTime(1400, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
812
|
+
self.assertEqual(jdt_custom_sep.isoformat(sep=" "), "1400-01-01 12:00:00+00:00")
|
|
813
|
+
|
|
814
|
+
jdt_custom_sep_offset = JalaliDateTime(1401, 5, 5, 10, 10, 10, tzinfo=tz_minus_8)
|
|
815
|
+
self.assertEqual(jdt_custom_sep_offset.isoformat(sep="_"), "1401-05-05_10:10:10-08:00")
|
|
816
|
+
|
|
817
|
+
# 8. Naive JalaliDateTime (no offset string)
|
|
818
|
+
jdt_naive = JalaliDateTime(1398, 10, 5, 12, 30, 0)
|
|
819
|
+
self.assertEqual(jdt_naive.isoformat(), "1398-10-05T12:30:00")
|
|
820
|
+
# With microseconds
|
|
821
|
+
jdt_naive_ms = JalaliDateTime(1398, 10, 5, 12, 30, 0, 123)
|
|
822
|
+
self.assertEqual(jdt_naive_ms.isoformat(), "1398-10-05T12:30:00.000123")
|
|
823
|
+
# With custom separator
|
|
824
|
+
self.assertEqual(jdt_naive.isoformat(sep=" "), "1398-10-05 12:30:00")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|