python-intl 0.4.0__tar.gz → 0.6.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.
- {python_intl-0.4.0 → python_intl-0.6.0}/PKG-INFO +27 -13
- {python_intl-0.4.0 → python_intl-0.6.0}/README.md +26 -12
- {python_intl-0.4.0 → python_intl-0.6.0}/pyproject.toml +6 -2
- {python_intl-0.4.0 → python_intl-0.6.0}/pyproject.toml.orig +3 -1
- {python_intl-0.4.0 → python_intl-0.6.0}/python_intl/__init__.py +5 -0
- python_intl-0.6.0/python_intl/_types.py +3 -0
- python_intl-0.6.0/python_intl/collator.py +126 -0
- {python_intl-0.4.0 → python_intl-0.6.0}/python_intl/datetimeformat.py +118 -4
- {python_intl-0.4.0 → python_intl-0.6.0}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-intl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Python implementation of the Intl JavaScript API
|
|
5
5
|
Author: David Danier
|
|
6
6
|
Author-email: David Danier <david.danier@gmail.com>
|
|
@@ -18,8 +18,7 @@ API similar to what the [`Intl` JavaScript API](https://developer.mozilla.org/en
|
|
|
18
18
|
provides. It is not meant to fully behave the same, but instead be close enough so
|
|
19
19
|
the "same" code works on JavaScript and Python, with similar results.
|
|
20
20
|
|
|
21
|
-
**Status:** This is very much work in progress.
|
|
22
|
-
exists and this also is not complete yet.
|
|
21
|
+
**Status:** This is very much work in progress.
|
|
23
22
|
|
|
24
23
|
**Note:** There are a lot of tests running the Python implementation against the
|
|
25
24
|
JavaScript one and comparing the results. In general things should be pretty
|
|
@@ -63,23 +62,38 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
63
62
|
import datetime as dt
|
|
64
63
|
import python_intl as Intl
|
|
65
64
|
|
|
65
|
+
# Format a datetime
|
|
66
66
|
datetime = dt.datetime(2026, 8, 15)
|
|
67
67
|
formatter = Intl.DateTimeFormat("de-DE", {"year": "numeric", "month": "2-digit", "day": "2-digit"})
|
|
68
|
-
formatter.format(datetime)
|
|
68
|
+
formatter.format(datetime)
|
|
69
|
+
# Result = "15.08.2026"
|
|
70
|
+
|
|
71
|
+
# Format a datetime range
|
|
69
72
|
datetime_till = dt.datetime(2026, 9, 7)
|
|
70
|
-
formatter.format_range(datetime, datetime_till)
|
|
73
|
+
formatter.format_range(datetime, datetime_till)
|
|
74
|
+
# Result = "15.08. – 07.09.2026"
|
|
71
75
|
```
|
|
72
76
|
|
|
73
77
|
#### Compatibility
|
|
74
78
|
|
|
75
|
-
| Method | Status | Python name
|
|
76
|
-
| ----------------------------------- | :----: |
|
|
77
|
-
| `DateTimeFormat.format` | ✅ |
|
|
78
|
-
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts`
|
|
79
|
-
| `DateTimeFormat.supportedLocalesOf` | ❌ |
|
|
80
|
-
| `DateTimeFormat.formatRange` | ✅ | `DateTimeFormat.format_range`
|
|
81
|
-
| `DateTimeFormat.formatRangeToParts` |
|
|
82
|
-
| `DateTimeFormat.resolvedOptions` | ❌ |
|
|
79
|
+
| Method | Status | Python name |
|
|
80
|
+
| ----------------------------------- | :----: | -------------------------------------- |
|
|
81
|
+
| `DateTimeFormat.format` | ✅ | |
|
|
82
|
+
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts` |
|
|
83
|
+
| `DateTimeFormat.supportedLocalesOf` | ❌ | |
|
|
84
|
+
| `DateTimeFormat.formatRange` | ✅ | `DateTimeFormat.format_range` |
|
|
85
|
+
| `DateTimeFormat.formatRangeToParts` | ✅ | `DateTimeFormat.format_range_to_parts` |
|
|
86
|
+
| `DateTimeFormat.resolvedOptions` | ❌ | |
|
|
87
|
+
|
|
88
|
+
### `Intl.Collator`
|
|
89
|
+
|
|
90
|
+
#### Compatibility
|
|
91
|
+
|
|
92
|
+
| Method | Status | Python name |
|
|
93
|
+
| ------------------ | :----: | ----------- |
|
|
94
|
+
| `Collator.compare` | ✅ | |
|
|
95
|
+
|
|
96
|
+
**Note:** Not all options are currently supported.
|
|
83
97
|
|
|
84
98
|
## Installation
|
|
85
99
|
|
|
@@ -5,8 +5,7 @@ API similar to what the [`Intl` JavaScript API](https://developer.mozilla.org/en
|
|
|
5
5
|
provides. It is not meant to fully behave the same, but instead be close enough so
|
|
6
6
|
the "same" code works on JavaScript and Python, with similar results.
|
|
7
7
|
|
|
8
|
-
**Status:** This is very much work in progress.
|
|
9
|
-
exists and this also is not complete yet.
|
|
8
|
+
**Status:** This is very much work in progress.
|
|
10
9
|
|
|
11
10
|
**Note:** There are a lot of tests running the Python implementation against the
|
|
12
11
|
JavaScript one and comparing the results. In general things should be pretty
|
|
@@ -50,23 +49,38 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
50
49
|
import datetime as dt
|
|
51
50
|
import python_intl as Intl
|
|
52
51
|
|
|
52
|
+
# Format a datetime
|
|
53
53
|
datetime = dt.datetime(2026, 8, 15)
|
|
54
54
|
formatter = Intl.DateTimeFormat("de-DE", {"year": "numeric", "month": "2-digit", "day": "2-digit"})
|
|
55
|
-
formatter.format(datetime)
|
|
55
|
+
formatter.format(datetime)
|
|
56
|
+
# Result = "15.08.2026"
|
|
57
|
+
|
|
58
|
+
# Format a datetime range
|
|
56
59
|
datetime_till = dt.datetime(2026, 9, 7)
|
|
57
|
-
formatter.format_range(datetime, datetime_till)
|
|
60
|
+
formatter.format_range(datetime, datetime_till)
|
|
61
|
+
# Result = "15.08. – 07.09.2026"
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
#### Compatibility
|
|
61
65
|
|
|
62
|
-
| Method | Status | Python name
|
|
63
|
-
| ----------------------------------- | :----: |
|
|
64
|
-
| `DateTimeFormat.format` | ✅ |
|
|
65
|
-
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts`
|
|
66
|
-
| `DateTimeFormat.supportedLocalesOf` | ❌ |
|
|
67
|
-
| `DateTimeFormat.formatRange` | ✅ | `DateTimeFormat.format_range`
|
|
68
|
-
| `DateTimeFormat.formatRangeToParts` |
|
|
69
|
-
| `DateTimeFormat.resolvedOptions` | ❌ |
|
|
66
|
+
| Method | Status | Python name |
|
|
67
|
+
| ----------------------------------- | :----: | -------------------------------------- |
|
|
68
|
+
| `DateTimeFormat.format` | ✅ | |
|
|
69
|
+
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts` |
|
|
70
|
+
| `DateTimeFormat.supportedLocalesOf` | ❌ | |
|
|
71
|
+
| `DateTimeFormat.formatRange` | ✅ | `DateTimeFormat.format_range` |
|
|
72
|
+
| `DateTimeFormat.formatRangeToParts` | ✅ | `DateTimeFormat.format_range_to_parts` |
|
|
73
|
+
| `DateTimeFormat.resolvedOptions` | ❌ | |
|
|
74
|
+
|
|
75
|
+
### `Intl.Collator`
|
|
76
|
+
|
|
77
|
+
#### Compatibility
|
|
78
|
+
|
|
79
|
+
| Method | Status | Python name |
|
|
80
|
+
| ------------------ | :----: | ----------- |
|
|
81
|
+
| `Collator.compare` | ✅ | |
|
|
82
|
+
|
|
83
|
+
**Note:** Not all options are currently supported.
|
|
70
84
|
|
|
71
85
|
## Installation
|
|
72
86
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-intl"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Python implementation of the Intl JavaScript API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,13 +21,17 @@ dev = [
|
|
|
21
21
|
"pyright>=1.1.411",
|
|
22
22
|
"pytest>=9.1.1",
|
|
23
23
|
"pytest-cov>=7.1.0",
|
|
24
|
+
"pytest-xdist>=3.8.0",
|
|
24
25
|
"ruff>=0.16.3",
|
|
25
26
|
"tox>=4.60.0",
|
|
26
27
|
"ty>=0.0.72",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[tool.pytest.ini_options]
|
|
30
|
-
markers = [
|
|
31
|
+
markers = [
|
|
32
|
+
"unit: mark a test as a unit test",
|
|
33
|
+
"node: mark tests jun against the node/JS implementation",
|
|
34
|
+
]
|
|
31
35
|
|
|
32
36
|
[tool.ruff]
|
|
33
37
|
line-length = 115
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-intl"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Python implementation of the Intl JavaScript API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -22,6 +22,7 @@ dev = [
|
|
|
22
22
|
"pyright>=1.1.411",
|
|
23
23
|
"pytest>=9.1.1",
|
|
24
24
|
"pytest-cov>=7.1.0",
|
|
25
|
+
"pytest-xdist>=3.8.0",
|
|
25
26
|
"ruff>=0.16.3",
|
|
26
27
|
"tox>=4.60.0",
|
|
27
28
|
"ty>=0.0.72",
|
|
@@ -30,6 +31,7 @@ dev = [
|
|
|
30
31
|
[tool.pytest.ini_options]
|
|
31
32
|
markers = [
|
|
32
33
|
"unit: mark a test as a unit test",
|
|
34
|
+
"node: mark tests jun against the node/JS implementation",
|
|
33
35
|
]
|
|
34
36
|
|
|
35
37
|
[tool.ruff]
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
from .collator import (
|
|
2
|
+
Collator as Collator,
|
|
3
|
+
CollatorOptions as CollatorOptions,
|
|
4
|
+
)
|
|
1
5
|
from .datetimeformat import (
|
|
2
6
|
DateTimeFormat as DateTimeFormat,
|
|
3
7
|
DateTimeFormatOptions as DateTimeFormatOptions,
|
|
8
|
+
DateTimeIntervalPatternPart as DateTimeIntervalPatternPart,
|
|
4
9
|
DateTimePatternPart as DateTimePatternPart,
|
|
5
10
|
FormatPatternNotFoundException as FormatPatternNotFoundException,
|
|
6
11
|
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
6
|
+
|
|
7
|
+
import icu # type: ignore[import-untyped]
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from typing import NotRequired, TypedDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ._types import LocaleMatcherT
|
|
15
|
+
|
|
16
|
+
# type UsageT = Literal["sort", "search"]
|
|
17
|
+
# type CollationT = Literal["emoji", "pinyin", "stroke"]
|
|
18
|
+
type CaseFirstT = Literal["upper", "lower", "false"]
|
|
19
|
+
type SensitivityT = Literal["base", "accent", "case", "variant"]
|
|
20
|
+
|
|
21
|
+
type ComparisonResultT = Literal[-1, 0, 1]
|
|
22
|
+
|
|
23
|
+
# Important: Must be the same as CollatorOptions
|
|
24
|
+
# (nothing is required, as this will be used to construct a
|
|
25
|
+
# CollatorOptions instance, so default values apply then)
|
|
26
|
+
class CollatorOptionsDictT(TypedDict):
|
|
27
|
+
locale_matcher: NotRequired[LocaleMatcherT]
|
|
28
|
+
# usage: NotRequired[UsageT | None]
|
|
29
|
+
# collation: NotRequired[CollationT | None]
|
|
30
|
+
numeric: NotRequired[bool]
|
|
31
|
+
case_first: NotRequired[CaseFirstT]
|
|
32
|
+
sensitivity: NotRequired[SensitivityT]
|
|
33
|
+
ignore_punctuation: NotRequired[bool | None]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
|
37
|
+
class CollatorOptions:
|
|
38
|
+
locale_matcher: LocaleMatcherT = "best fit"
|
|
39
|
+
# usage: UsageT | None = None
|
|
40
|
+
# collation: CollationT | None = None
|
|
41
|
+
numeric: bool = False
|
|
42
|
+
case_first: CaseFirstT = "false"
|
|
43
|
+
sensitivity: SensitivityT = "variant"
|
|
44
|
+
ignore_punctuation: bool | None = None
|
|
45
|
+
|
|
46
|
+
def to_json(self) -> dict[str, str | int]:
|
|
47
|
+
return {
|
|
48
|
+
k: v
|
|
49
|
+
for k, v in (
|
|
50
|
+
("localeMatcher", self.locale_matcher),
|
|
51
|
+
# ("usage", self.usage),
|
|
52
|
+
# ("collation", self.collation),
|
|
53
|
+
("numeric", self.numeric),
|
|
54
|
+
("caseFirst", self.case_first),
|
|
55
|
+
("sensitivity", self.sensitivity),
|
|
56
|
+
("ignorePunctuation", self.ignore_punctuation),
|
|
57
|
+
)
|
|
58
|
+
if v is not None
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_COLLATOR_RESULT_TO_RESULT: dict[icu.UCollationResult, ComparisonResultT] = { # ty: ignore[unresolved-attribute]
|
|
63
|
+
icu.UCollationResult.LESS: -1, # ty: ignore[unresolved-attribute]
|
|
64
|
+
icu.UCollationResult.EQUAL: 0, # ty: ignore[unresolved-attribute]
|
|
65
|
+
icu.UCollationResult.GREATER: 1, # ty: ignore[unresolved-attribute]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Collator:
|
|
70
|
+
locale: str
|
|
71
|
+
options: CollatorOptions
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
locale: str,
|
|
76
|
+
options: CollatorOptions | CollatorOptionsDictT | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
self.locale = locale
|
|
79
|
+
if options is None:
|
|
80
|
+
self.options = CollatorOptions()
|
|
81
|
+
elif isinstance(options, CollatorOptions):
|
|
82
|
+
self.options = options
|
|
83
|
+
else:
|
|
84
|
+
self.options = CollatorOptions(**options)
|
|
85
|
+
|
|
86
|
+
@cached_property
|
|
87
|
+
def _icu_locale(self) -> icu.Locale: # ty: ignore[unresolved-attribute]
|
|
88
|
+
return icu.Locale(self.locale) # ty: ignore[unresolved-attribute]
|
|
89
|
+
|
|
90
|
+
@cached_property
|
|
91
|
+
def _icu_collator(self) -> icu.Collator: # ty: ignore[unresolved-attribute]
|
|
92
|
+
collator = icu.Collator.createInstance(self._icu_locale) # ty: ignore[unresolved-attribute]
|
|
93
|
+
|
|
94
|
+
if self.options.numeric:
|
|
95
|
+
collator.setAttribute(icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON) # ty: ignore[unresolved-attribute]
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
self.options.ignore_punctuation
|
|
99
|
+
or (
|
|
100
|
+
self.options.ignore_punctuation is None
|
|
101
|
+
and self._icu_locale.getLanguage() == "th"
|
|
102
|
+
)
|
|
103
|
+
):
|
|
104
|
+
collator.setAttribute(icu.UCollAttribute.ALTERNATE_HANDLING, icu.UCollAttributeValue.SHIFTED) # ty: ignore[unresolved-attribute]
|
|
105
|
+
|
|
106
|
+
match self.options.case_first:
|
|
107
|
+
case "upper":
|
|
108
|
+
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.UPPER_FIRST) # ty: ignore[unresolved-attribute]
|
|
109
|
+
case "lower":
|
|
110
|
+
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.LOWER_FIRST) # ty: ignore[unresolved-attribute]
|
|
111
|
+
|
|
112
|
+
collator.setAttribute(icu.UCollAttribute.NORMALIZATION_MODE, icu.UCollAttributeValue.ON) # ty: ignore[unresolved-attribute]
|
|
113
|
+
match self.options.sensitivity:
|
|
114
|
+
case "base":
|
|
115
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.PRIMARY) # ty: ignore[unresolved-attribute]
|
|
116
|
+
case "accent":
|
|
117
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.SECONDARY) # ty: ignore[unresolved-attribute]
|
|
118
|
+
case "case":
|
|
119
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.TERTIARY) # ty: ignore[unresolved-attribute]
|
|
120
|
+
case "variant":
|
|
121
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.QUATERNARY) # ty: ignore[unresolved-attribute]
|
|
122
|
+
|
|
123
|
+
return collator
|
|
124
|
+
|
|
125
|
+
def compare(self, string_a: str, string_b: str) -> ComparisonResultT:
|
|
126
|
+
return _COLLATOR_RESULT_TO_RESULT[self._icu_collator.compare(string_a, string_b)]
|
|
@@ -13,7 +13,8 @@ if TYPE_CHECKING:
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
|
-
|
|
16
|
+
from ._types import LocaleMatcherT
|
|
17
|
+
|
|
17
18
|
type Hour12T = bool | None
|
|
18
19
|
type HourCycleT = Literal["h11", "h12", "h23", "h24"] | None
|
|
19
20
|
|
|
@@ -107,6 +108,22 @@ _PATTERN_SYMBOL_TO_TYPE: dict[str, PatternPartTypeT] = {
|
|
|
107
108
|
"x": "time_zone_name",
|
|
108
109
|
"X": "time_zone_name",
|
|
109
110
|
}
|
|
111
|
+
_PATTERN_FIELD_TO_TYPE: dict[icu.UDateTimePatternField, PatternPartTypeT] = { # ty: ignore[unresolved-attribute]
|
|
112
|
+
icu.DateFormat.ERA_FIELD: "era", # ty: ignore[unresolved-attribute]
|
|
113
|
+
icu.DateFormat.YEAR_FIELD: "year", # ty: ignore[unresolved-attribute]
|
|
114
|
+
icu.DateFormat.MONTH_FIELD: "month", # ty: ignore[unresolved-attribute]
|
|
115
|
+
icu.DateFormat.DAY_OF_WEEK_FIELD: "weekday", # ty: ignore[unresolved-attribute]
|
|
116
|
+
icu.DateFormat.DATE_FIELD: "day", # ty: ignore[unresolved-attribute]
|
|
117
|
+
icu.DateFormat.AM_PM_FIELD: "day_period", # ty: ignore[unresolved-attribute]
|
|
118
|
+
icu.DateFormat.HOUR0_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
119
|
+
icu.DateFormat.HOUR1_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
120
|
+
icu.DateFormat.HOUR_OF_DAY0_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
121
|
+
icu.DateFormat.HOUR_OF_DAY1_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
122
|
+
icu.DateFormat.MINUTE_FIELD: "minute", # ty: ignore[unresolved-attribute]
|
|
123
|
+
icu.DateFormat.SECOND_FIELD: "second", # ty: ignore[unresolved-attribute]
|
|
124
|
+
icu.DateFormat.MILLISECOND_FIELD: "fraction_second_digits", # ty: ignore[unresolved-attribute]
|
|
125
|
+
icu.DateFormat.TIMEZONE_FIELD: "time_zone_name", # ty: ignore[unresolved-attribute]
|
|
126
|
+
}
|
|
110
127
|
_PATTERN_QUOTE = "'"
|
|
111
128
|
|
|
112
129
|
_COMPONENT_TO_JSON_MAP: dict[str, str] = {
|
|
@@ -120,6 +137,10 @@ _TIMEZONE_NAME_TO_JSON_MAP: dict[str | None, str] = {
|
|
|
120
137
|
"short_generic": "shortGeneric",
|
|
121
138
|
"long_generic": "longGeneric",
|
|
122
139
|
}
|
|
140
|
+
_SOURCE_TO_JSON_MAP: dict[str, str] = {
|
|
141
|
+
"start_range": "startRange",
|
|
142
|
+
"end_range": "endRange",
|
|
143
|
+
}
|
|
123
144
|
|
|
124
145
|
|
|
125
146
|
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
|
@@ -314,6 +335,20 @@ class DateTimePatternPart:
|
|
|
314
335
|
}
|
|
315
336
|
|
|
316
337
|
|
|
338
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
339
|
+
class DateTimeIntervalPatternPart:
|
|
340
|
+
type: PatternPartTypeT
|
|
341
|
+
value: str
|
|
342
|
+
source: Literal["start_range", "end_range", "shared"]
|
|
343
|
+
|
|
344
|
+
def to_json(self) -> dict[str, str]:
|
|
345
|
+
return {
|
|
346
|
+
"type": _COMPONENT_TO_JSON_MAP.get(self.type, self.type),
|
|
347
|
+
"value": self.value,
|
|
348
|
+
"source": _SOURCE_TO_JSON_MAP.get(self.source, self.source),
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
317
352
|
@cache
|
|
318
353
|
def _options_to_format_pattern(
|
|
319
354
|
locale: icu.Locale, # ty: ignore[unresolved-attribute]
|
|
@@ -345,6 +380,23 @@ def _options_to_format_pattern(
|
|
|
345
380
|
raise FormatPatternNotFoundException("Didn't find pattern for desired options")
|
|
346
381
|
|
|
347
382
|
|
|
383
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
384
|
+
class _PartSpan:
|
|
385
|
+
start: int
|
|
386
|
+
end: int
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
def empty(cls) -> _PartSpan:
|
|
390
|
+
return cls(start=0, end=0)
|
|
391
|
+
|
|
392
|
+
@classmethod
|
|
393
|
+
def from_constrained_fieldposition(cls, position: icu.ConstrainedFieldPosition) -> _PartSpan: # ty: ignore[unresolved-attribute]
|
|
394
|
+
return cls(start=position.getStart(), end=position.getLimit())
|
|
395
|
+
|
|
396
|
+
def __contains__(self, inner: _PartSpan) -> bool:
|
|
397
|
+
return inner.start >= self.start and inner.end <= self.end
|
|
398
|
+
|
|
399
|
+
|
|
348
400
|
class DateTimeFormat:
|
|
349
401
|
locale: str
|
|
350
402
|
options: DateTimeFormatOptions
|
|
@@ -352,10 +404,12 @@ class DateTimeFormat:
|
|
|
352
404
|
def __init__(
|
|
353
405
|
self,
|
|
354
406
|
locale: str,
|
|
355
|
-
options: DateTimeFormatOptions | DateTimeFormatOptionsDictT,
|
|
407
|
+
options: DateTimeFormatOptions | DateTimeFormatOptionsDictT | None = None,
|
|
356
408
|
) -> None:
|
|
357
409
|
self.locale = locale
|
|
358
|
-
if
|
|
410
|
+
if options is None:
|
|
411
|
+
self.options = DateTimeFormatOptions()
|
|
412
|
+
elif isinstance(options, DateTimeFormatOptions):
|
|
359
413
|
self.options = options
|
|
360
414
|
else:
|
|
361
415
|
self.options = DateTimeFormatOptions(**options)
|
|
@@ -437,6 +491,66 @@ class DateTimeFormat:
|
|
|
437
491
|
possible_skeletons = list(_options_to_possible_skeletons(self.options))
|
|
438
492
|
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self._icu_locale) # ty: ignore[unresolved-attribute]
|
|
439
493
|
|
|
440
|
-
def format_range(
|
|
494
|
+
def format_range(
|
|
495
|
+
self,
|
|
496
|
+
start_datetime: dt.datetime,
|
|
497
|
+
end_datetime: dt.datetime,
|
|
498
|
+
) -> str:
|
|
441
499
|
icu_date_interval = icu.DateInterval(start_datetime, end_datetime) # ty: ignore[unresolved-attribute]
|
|
442
500
|
return self._icu_dateinterval_format.format(icu_date_interval)
|
|
501
|
+
|
|
502
|
+
def format_range_to_parts(
|
|
503
|
+
self,
|
|
504
|
+
start_datetime: dt.datetime,
|
|
505
|
+
end_datetime: dt.datetime,
|
|
506
|
+
) -> Iterable[DateTimeIntervalPatternPart]:
|
|
507
|
+
icu_date_interval = icu.DateInterval(start_datetime, end_datetime) # ty: ignore[unresolved-attribute]
|
|
508
|
+
formatted = self._icu_dateinterval_format.formatToValue(icu_date_interval)
|
|
509
|
+
|
|
510
|
+
# Find spans of both datetimes (used to determine which parts have which source)
|
|
511
|
+
span_start = _PartSpan.empty()
|
|
512
|
+
span_end = _PartSpan.empty()
|
|
513
|
+
for part in formatted:
|
|
514
|
+
if part.getCategory() == icu.UFieldCategory.DATE_INTERVAL_SPAN: # ty: ignore[unresolved-attribute]
|
|
515
|
+
match part.getField():
|
|
516
|
+
case 0:
|
|
517
|
+
span_start = _PartSpan.from_constrained_fieldposition(part)
|
|
518
|
+
case 1:
|
|
519
|
+
span_end = _PartSpan.from_constrained_fieldposition(part)
|
|
520
|
+
|
|
521
|
+
def source_of(span: _PartSpan) -> Literal["start_range", "end_range", "shared"]:
|
|
522
|
+
if span in span_start:
|
|
523
|
+
return "start_range"
|
|
524
|
+
elif span in span_end:
|
|
525
|
+
return "end_range"
|
|
526
|
+
else:
|
|
527
|
+
return "shared"
|
|
528
|
+
|
|
529
|
+
# Break result string into parts
|
|
530
|
+
result_string = str(formatted)
|
|
531
|
+
last_end = 0
|
|
532
|
+
for part in formatted:
|
|
533
|
+
span = _PartSpan.from_constrained_fieldposition(part)
|
|
534
|
+
if span.start > last_end:
|
|
535
|
+
yield DateTimeIntervalPatternPart(
|
|
536
|
+
type="literal",
|
|
537
|
+
value=result_string[last_end:span.start],
|
|
538
|
+
source=source_of(_PartSpan(start=last_end, end=span.start)),
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if part.getCategory() == icu.UFieldCategory.DATE: # ty: ignore[unresolved-attribute]
|
|
542
|
+
yield DateTimeIntervalPatternPart(
|
|
543
|
+
type=_PATTERN_FIELD_TO_TYPE.get(part.getField(), "unknown"),
|
|
544
|
+
value=result_string[span.start:span.end],
|
|
545
|
+
source=source_of(span),
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
last_end = span.end
|
|
549
|
+
|
|
550
|
+
# Ensure we didn't miss anything at the end
|
|
551
|
+
if last_end < len(result_string):
|
|
552
|
+
yield DateTimeIntervalPatternPart(
|
|
553
|
+
type="literal",
|
|
554
|
+
value=result_string[last_end:],
|
|
555
|
+
source=source_of(_PartSpan(start=last_end, end=len(result_string))),
|
|
556
|
+
)
|
|
File without changes
|