python-intl 0.6.0__tar.gz → 0.8.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.6.0 → python_intl-0.8.0}/PKG-INFO +48 -1
- {python_intl-0.6.0 → python_intl-0.8.0}/README.md +47 -0
- {python_intl-0.6.0 → python_intl-0.8.0}/pyproject.toml +5 -2
- {python_intl-0.6.0 → python_intl-0.8.0}/pyproject.toml.orig +5 -3
- python_intl-0.8.0/python_intl/_types.py +34 -0
- {python_intl-0.6.0 → python_intl-0.8.0}/python_intl/collator.py +28 -20
- {python_intl-0.6.0 → python_intl-0.8.0}/python_intl/datetimeformat.py +46 -56
- python_intl-0.8.0/python_intl/locale.py +16 -0
- python_intl-0.6.0/python_intl/_types.py +0 -3
- {python_intl-0.6.0 → python_intl-0.8.0}/LICENSE +0 -0
- {python_intl-0.6.0 → python_intl-0.8.0}/python_intl/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-intl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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>
|
|
@@ -54,6 +54,23 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
54
54
|
|
|
55
55
|
## Available `Intl` classes
|
|
56
56
|
|
|
57
|
+
### `Intl.Locale`
|
|
58
|
+
|
|
59
|
+
#### Example usage
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import datetime as dt
|
|
63
|
+
import python_intl as Intl
|
|
64
|
+
|
|
65
|
+
locale = Intl.DateTimeFormat("de-DE")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Compatibility
|
|
69
|
+
|
|
70
|
+
`Locale` currently does not support any methods or options. It is mainly there to
|
|
71
|
+
allow using the locale class instead of a string with the other classes. There will
|
|
72
|
+
be more later.
|
|
73
|
+
|
|
57
74
|
### `Intl.DateTimeFormat`
|
|
58
75
|
|
|
59
76
|
#### Example usage
|
|
@@ -87,6 +104,20 @@ formatter.format_range(datetime, datetime_till)
|
|
|
87
104
|
|
|
88
105
|
### `Intl.Collator`
|
|
89
106
|
|
|
107
|
+
#### Example usage
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
import python_intl as Intl
|
|
111
|
+
|
|
112
|
+
collator = Intl.Collator("de-DE", {"numeric": True})
|
|
113
|
+
collator.compare("10", "9")
|
|
114
|
+
# Result = 1, which means 9 comes before 10
|
|
115
|
+
|
|
116
|
+
# You can also use sorted, although this is not available in JavaScript
|
|
117
|
+
collator.sorted(["10", "9"])
|
|
118
|
+
# Result = ['9', '10']
|
|
119
|
+
```
|
|
120
|
+
|
|
90
121
|
#### Compatibility
|
|
91
122
|
|
|
92
123
|
| Method | Status | Python name |
|
|
@@ -95,6 +126,12 @@ formatter.format_range(datetime, datetime_till)
|
|
|
95
126
|
|
|
96
127
|
**Note:** Not all options are currently supported.
|
|
97
128
|
|
|
129
|
+
#### Additional methods
|
|
130
|
+
|
|
131
|
+
* `Collator.sorted`: Somewhat like `sorted`, but uses the collator for comparison. If you want
|
|
132
|
+
to sort complex datastructures you can provide a `key` parameter (like with `sorted`) to
|
|
133
|
+
get a comparable string value (for example by returning an attribute).
|
|
134
|
+
|
|
98
135
|
## Installation
|
|
99
136
|
|
|
100
137
|
Be sure to be able to install `PyICU`, see the installation docs there:
|
|
@@ -105,3 +142,13 @@ setting `PKG_CONFIG_PATH` accordingly helps most of the time I guess.
|
|
|
105
142
|
|
|
106
143
|
When this is done you should be able to install `python-intl` using any package
|
|
107
144
|
manager, like `pip install python-intl` or `uv add python-intl`.
|
|
145
|
+
|
|
146
|
+
## Contributing
|
|
147
|
+
|
|
148
|
+
If you want to contribute to this project, feel free to just fork the project,
|
|
149
|
+
create a dev branch in your fork and then create a pull request (PR). If you
|
|
150
|
+
are unsure about whether your changes really suits the project please create an
|
|
151
|
+
issue first, to talk about this.
|
|
152
|
+
|
|
153
|
+
Please do not contribute AI generated code unless we explicitly talked about
|
|
154
|
+
this and agreed upon doing so first. In general I do not want AI contributions.
|
|
@@ -41,6 +41,23 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
41
41
|
|
|
42
42
|
## Available `Intl` classes
|
|
43
43
|
|
|
44
|
+
### `Intl.Locale`
|
|
45
|
+
|
|
46
|
+
#### Example usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import datetime as dt
|
|
50
|
+
import python_intl as Intl
|
|
51
|
+
|
|
52
|
+
locale = Intl.DateTimeFormat("de-DE")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Compatibility
|
|
56
|
+
|
|
57
|
+
`Locale` currently does not support any methods or options. It is mainly there to
|
|
58
|
+
allow using the locale class instead of a string with the other classes. There will
|
|
59
|
+
be more later.
|
|
60
|
+
|
|
44
61
|
### `Intl.DateTimeFormat`
|
|
45
62
|
|
|
46
63
|
#### Example usage
|
|
@@ -74,6 +91,20 @@ formatter.format_range(datetime, datetime_till)
|
|
|
74
91
|
|
|
75
92
|
### `Intl.Collator`
|
|
76
93
|
|
|
94
|
+
#### Example usage
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import python_intl as Intl
|
|
98
|
+
|
|
99
|
+
collator = Intl.Collator("de-DE", {"numeric": True})
|
|
100
|
+
collator.compare("10", "9")
|
|
101
|
+
# Result = 1, which means 9 comes before 10
|
|
102
|
+
|
|
103
|
+
# You can also use sorted, although this is not available in JavaScript
|
|
104
|
+
collator.sorted(["10", "9"])
|
|
105
|
+
# Result = ['9', '10']
|
|
106
|
+
```
|
|
107
|
+
|
|
77
108
|
#### Compatibility
|
|
78
109
|
|
|
79
110
|
| Method | Status | Python name |
|
|
@@ -82,6 +113,12 @@ formatter.format_range(datetime, datetime_till)
|
|
|
82
113
|
|
|
83
114
|
**Note:** Not all options are currently supported.
|
|
84
115
|
|
|
116
|
+
#### Additional methods
|
|
117
|
+
|
|
118
|
+
* `Collator.sorted`: Somewhat like `sorted`, but uses the collator for comparison. If you want
|
|
119
|
+
to sort complex datastructures you can provide a `key` parameter (like with `sorted`) to
|
|
120
|
+
get a comparable string value (for example by returning an attribute).
|
|
121
|
+
|
|
85
122
|
## Installation
|
|
86
123
|
|
|
87
124
|
Be sure to be able to install `PyICU`, see the installation docs there:
|
|
@@ -92,3 +129,13 @@ setting `PKG_CONFIG_PATH` accordingly helps most of the time I guess.
|
|
|
92
129
|
|
|
93
130
|
When this is done you should be able to install `python-intl` using any package
|
|
94
131
|
manager, like `pip install python-intl` or `uv add python-intl`.
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
If you want to contribute to this project, feel free to just fork the project,
|
|
136
|
+
create a dev branch in your fork and then create a pull request (PR). If you
|
|
137
|
+
are unsure about whether your changes really suits the project please create an
|
|
138
|
+
issue first, to talk about this.
|
|
139
|
+
|
|
140
|
+
Please do not contribute AI generated code unless we explicitly talked about
|
|
141
|
+
this and agreed upon doing so first. In general I do not want AI contributions.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-intl"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Python implementation of the Intl JavaScript API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -35,7 +35,6 @@ markers = [
|
|
|
35
35
|
|
|
36
36
|
[tool.ruff]
|
|
37
37
|
line-length = 115
|
|
38
|
-
target-version = "py312"
|
|
39
38
|
output-format = "grouped"
|
|
40
39
|
|
|
41
40
|
[tool.ruff.lint]
|
|
@@ -69,6 +68,7 @@ ignore = [
|
|
|
69
68
|
"B008",
|
|
70
69
|
"F405",
|
|
71
70
|
"F821",
|
|
71
|
+
"TD003",
|
|
72
72
|
]
|
|
73
73
|
|
|
74
74
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -88,6 +88,9 @@ ignore = [
|
|
|
88
88
|
combine-as-imports = true
|
|
89
89
|
force-wrap-aliases = true
|
|
90
90
|
|
|
91
|
+
[tool.pyright]
|
|
92
|
+
pythonVersion = "3.12"
|
|
93
|
+
|
|
91
94
|
[tool.uv.build-backend]
|
|
92
95
|
module-name = "python_intl"
|
|
93
96
|
module-root = ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-intl"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Python implementation of the Intl JavaScript API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -36,12 +36,11 @@ markers = [
|
|
|
36
36
|
|
|
37
37
|
[tool.ruff]
|
|
38
38
|
line-length = 115
|
|
39
|
-
target-version = "py312"
|
|
40
39
|
output-format = "grouped"
|
|
41
40
|
|
|
42
41
|
[tool.ruff.lint]
|
|
43
42
|
select = ["F","E","W","C","I","N","UP","ANN","S","B","A","COM","C4","T20","PT","ARG","TD","RUF"]
|
|
44
|
-
ignore = ["A001","A002","A003","ANN401","C901","N8","B008","F405","F821"]
|
|
43
|
+
ignore = ["A001","A002","A003","ANN401","C901","N8","B008","F405","F821","TD003"]
|
|
45
44
|
|
|
46
45
|
[tool.ruff.lint.per-file-ignores]
|
|
47
46
|
"__init__.py" = ["F401"]
|
|
@@ -52,6 +51,9 @@ ignore = ["A001","A002","A003","ANN401","C901","N8","B008","F405","F821"]
|
|
|
52
51
|
combine-as-imports = true
|
|
53
52
|
force-wrap-aliases = true
|
|
54
53
|
|
|
54
|
+
[tool.pyright]
|
|
55
|
+
pythonVersion = "3.12"
|
|
56
|
+
|
|
55
57
|
[build-system]
|
|
56
58
|
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
57
59
|
build-backend = "uv_build"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
type LocaleMatcherT = Literal["best fit", "lookup"]
|
|
7
|
+
type Hour12T = bool
|
|
8
|
+
type HourCycleT = Literal["h11", "h12", "h23", "h24"]
|
|
9
|
+
|
|
10
|
+
type EraFormatT = Literal["long", "short", "narrow"]
|
|
11
|
+
type YearFormatT = Literal["numeric", "2-digit"]
|
|
12
|
+
type MonthFormatT = Literal["numeric", "2-digit", "long", "short", "narrow"]
|
|
13
|
+
type WeekdayFormatT = Literal["long", "short", "narrow"]
|
|
14
|
+
type DayFormatT = Literal["numeric", "2-digit"]
|
|
15
|
+
type DayPeriodFormatT = Literal["long", "short", "narrow"]
|
|
16
|
+
type HourFormatT = Literal["numeric", "2-digit"]
|
|
17
|
+
type MinuteFormatT = Literal["numeric", "2-digit"]
|
|
18
|
+
type SecondFormatT = Literal["numeric", "2-digit"]
|
|
19
|
+
type FractionSecondDigitsFormatT = Literal[1, 2, 3]
|
|
20
|
+
type TimezoneNameFormatT = Literal[
|
|
21
|
+
"short",
|
|
22
|
+
"long",
|
|
23
|
+
"short_offset",
|
|
24
|
+
"long_offset",
|
|
25
|
+
"short_generic",
|
|
26
|
+
"long_generic",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# type UsageT = Literal["sort", "search"]
|
|
30
|
+
# type CollationT = Literal["emoji", "pinyin", "stroke"]
|
|
31
|
+
type CaseFirstT = Literal["upper", "lower", "false"]
|
|
32
|
+
type SensitivityT = Literal["base", "accent", "case", "variant"]
|
|
33
|
+
|
|
34
|
+
type ComparisonResultT = Literal[-1, 0, 1]
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
+
import functools
|
|
5
|
+
from collections.abc import Callable, Iterable
|
|
4
6
|
from functools import cached_property
|
|
5
|
-
from typing import TYPE_CHECKING,
|
|
7
|
+
from typing import TYPE_CHECKING, overload
|
|
6
8
|
|
|
7
9
|
import icu # type: ignore[import-untyped]
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
from typing import NotRequired, TypedDict
|
|
11
|
-
|
|
11
|
+
from .locale import Locale
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from
|
|
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"]
|
|
14
|
+
from typing import NotRequired, TypedDict
|
|
20
15
|
|
|
21
|
-
|
|
16
|
+
from ._types import CaseFirstT, ComparisonResultT, LocaleMatcherT, SensitivityT
|
|
22
17
|
|
|
23
18
|
# Important: Must be the same as CollatorOptions
|
|
24
19
|
# (nothing is required, as this will be used to construct a
|
|
@@ -67,15 +62,18 @@ _COLLATOR_RESULT_TO_RESULT: dict[icu.UCollationResult, ComparisonResultT] = { #
|
|
|
67
62
|
|
|
68
63
|
|
|
69
64
|
class Collator:
|
|
70
|
-
locale:
|
|
65
|
+
locale: Locale
|
|
71
66
|
options: CollatorOptions
|
|
72
67
|
|
|
73
68
|
def __init__(
|
|
74
69
|
self,
|
|
75
|
-
locale: str,
|
|
70
|
+
locale: Locale | str,
|
|
76
71
|
options: CollatorOptions | CollatorOptionsDictT | None = None,
|
|
77
72
|
) -> None:
|
|
78
|
-
|
|
73
|
+
if isinstance(locale, Locale):
|
|
74
|
+
self.locale = locale
|
|
75
|
+
else:
|
|
76
|
+
self.locale = Locale(locale)
|
|
79
77
|
if options is None:
|
|
80
78
|
self.options = CollatorOptions()
|
|
81
79
|
elif isinstance(options, CollatorOptions):
|
|
@@ -83,13 +81,9 @@ class Collator:
|
|
|
83
81
|
else:
|
|
84
82
|
self.options = CollatorOptions(**options)
|
|
85
83
|
|
|
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
84
|
@cached_property
|
|
91
85
|
def _icu_collator(self) -> icu.Collator: # ty: ignore[unresolved-attribute]
|
|
92
|
-
collator = icu.Collator.createInstance(self._icu_locale) # ty: ignore[unresolved-attribute]
|
|
86
|
+
collator = icu.Collator.createInstance(self.locale._icu_locale) # ty: ignore[unresolved-attribute]
|
|
93
87
|
|
|
94
88
|
if self.options.numeric:
|
|
95
89
|
collator.setAttribute(icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON) # ty: ignore[unresolved-attribute]
|
|
@@ -98,7 +92,7 @@ class Collator:
|
|
|
98
92
|
self.options.ignore_punctuation
|
|
99
93
|
or (
|
|
100
94
|
self.options.ignore_punctuation is None
|
|
101
|
-
and self._icu_locale.getLanguage() == "th"
|
|
95
|
+
and self.locale._icu_locale.getLanguage() == "th"
|
|
102
96
|
)
|
|
103
97
|
):
|
|
104
98
|
collator.setAttribute(icu.UCollAttribute.ALTERNATE_HANDLING, icu.UCollAttributeValue.SHIFTED) # ty: ignore[unresolved-attribute]
|
|
@@ -124,3 +118,17 @@ class Collator:
|
|
|
124
118
|
|
|
125
119
|
def compare(self, string_a: str, string_b: str) -> ComparisonResultT:
|
|
126
120
|
return _COLLATOR_RESULT_TO_RESULT[self._icu_collator.compare(string_a, string_b)]
|
|
121
|
+
|
|
122
|
+
@overload
|
|
123
|
+
def sorted(self, items: Iterable[str], /, key: None = None) -> Iterable[str]: ...
|
|
124
|
+
@overload
|
|
125
|
+
def sorted[T](self, items: Iterable[T], /, key: Callable[[T], str]) -> Iterable[T]: ...
|
|
126
|
+
def sorted(self, items, /, key = None):
|
|
127
|
+
return sorted(
|
|
128
|
+
items,
|
|
129
|
+
key=functools.cmp_to_key(
|
|
130
|
+
(lambda a, b: self.compare(key(a), key(b)))
|
|
131
|
+
if key
|
|
132
|
+
else self.compare,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
@@ -7,50 +7,30 @@ from typing import TYPE_CHECKING, Literal
|
|
|
7
7
|
|
|
8
8
|
import icu # type: ignore[import-untyped]
|
|
9
9
|
|
|
10
|
+
from .locale import Locale
|
|
11
|
+
|
|
10
12
|
if TYPE_CHECKING:
|
|
11
13
|
from collections.abc import Iterable
|
|
12
14
|
from typing import NotRequired, TypedDict
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
type SecondFormatT = Literal["numeric", "2-digit"]
|
|
30
|
-
type FractionSecondDigitsFormatT = Literal[1, 2, 3]
|
|
31
|
-
type TimezoneNameFormatT = Literal[
|
|
32
|
-
"short",
|
|
33
|
-
"long",
|
|
34
|
-
"short_offset",
|
|
35
|
-
"long_offset",
|
|
36
|
-
"short_generic",
|
|
37
|
-
"long_generic",
|
|
38
|
-
]
|
|
39
|
-
type AnyFormatT = (
|
|
40
|
-
EraFormatT
|
|
41
|
-
| YearFormatT
|
|
42
|
-
| MonthFormatT
|
|
43
|
-
| WeekdayFormatT
|
|
44
|
-
| DayFormatT
|
|
45
|
-
| DayPeriodFormatT
|
|
46
|
-
| HourFormatT
|
|
47
|
-
| MinuteFormatT
|
|
48
|
-
| SecondFormatT
|
|
49
|
-
| FractionSecondDigitsFormatT
|
|
50
|
-
| TimezoneNameFormatT
|
|
16
|
+
from ._types import (
|
|
17
|
+
DayFormatT,
|
|
18
|
+
DayPeriodFormatT,
|
|
19
|
+
EraFormatT,
|
|
20
|
+
FractionSecondDigitsFormatT,
|
|
21
|
+
Hour12T,
|
|
22
|
+
HourCycleT,
|
|
23
|
+
HourFormatT,
|
|
24
|
+
LocaleMatcherT,
|
|
25
|
+
MinuteFormatT,
|
|
26
|
+
MonthFormatT,
|
|
27
|
+
SecondFormatT,
|
|
28
|
+
TimezoneNameFormatT,
|
|
29
|
+
WeekdayFormatT,
|
|
30
|
+
YearFormatT,
|
|
51
31
|
)
|
|
52
32
|
|
|
53
|
-
type
|
|
33
|
+
type DatetimePatternPartTypeT = Literal[
|
|
54
34
|
"literal", "unknown",
|
|
55
35
|
"era", "year", "month", "weekday", "day", "day_period",
|
|
56
36
|
"hour", "minute", "second", "fraction_second_digits",
|
|
@@ -78,7 +58,7 @@ if TYPE_CHECKING:
|
|
|
78
58
|
time_zone_name: NotRequired[TimezoneNameFormatT]
|
|
79
59
|
|
|
80
60
|
_PATTERN_SYMBOLS = "GyYuUrQqMLqQdDFgEecabBhHkKmsSAzZOvVxX" # includes unused
|
|
81
|
-
_PATTERN_SYMBOL_TO_TYPE: dict[str,
|
|
61
|
+
_PATTERN_SYMBOL_TO_TYPE: dict[str, DatetimePatternPartTypeT] = {
|
|
82
62
|
"G": "era",
|
|
83
63
|
"y": "year",
|
|
84
64
|
"Y": "year",
|
|
@@ -92,6 +72,7 @@ _PATTERN_SYMBOL_TO_TYPE: dict[str, PatternPartTypeT] = {
|
|
|
92
72
|
"d": "day",
|
|
93
73
|
"a": "day_period",
|
|
94
74
|
"b": "day_period",
|
|
75
|
+
"B": "day_period",
|
|
95
76
|
"C": "day_period",
|
|
96
77
|
"h": "hour",
|
|
97
78
|
"H": "hour",
|
|
@@ -108,13 +89,19 @@ _PATTERN_SYMBOL_TO_TYPE: dict[str, PatternPartTypeT] = {
|
|
|
108
89
|
"x": "time_zone_name",
|
|
109
90
|
"X": "time_zone_name",
|
|
110
91
|
}
|
|
111
|
-
_PATTERN_FIELD_TO_TYPE: dict[icu.UDateTimePatternField,
|
|
92
|
+
_PATTERN_FIELD_TO_TYPE: dict[icu.UDateTimePatternField, DatetimePatternPartTypeT] = { # ty: ignore[unresolved-attribute]
|
|
112
93
|
icu.DateFormat.ERA_FIELD: "era", # ty: ignore[unresolved-attribute]
|
|
113
94
|
icu.DateFormat.YEAR_FIELD: "year", # ty: ignore[unresolved-attribute]
|
|
114
95
|
icu.DateFormat.MONTH_FIELD: "month", # ty: ignore[unresolved-attribute]
|
|
115
96
|
icu.DateFormat.DAY_OF_WEEK_FIELD: "weekday", # ty: ignore[unresolved-attribute]
|
|
116
97
|
icu.DateFormat.DATE_FIELD: "day", # ty: ignore[unresolved-attribute]
|
|
117
98
|
icu.DateFormat.AM_PM_FIELD: "day_period", # ty: ignore[unresolved-attribute]
|
|
99
|
+
# TODO(ddanier): Use this instead once PyICU has those values in the enum:
|
|
100
|
+
# https://gitlab.pyicu.org/main/pyicu/-/work_items/180
|
|
101
|
+
# icu.DateFormat.AM_PM_MIDNIGHT_NOON_FIELD: "day_period",
|
|
102
|
+
# icu.DateFormat.FLEXIBLE_DAY_PERIOD_FIELD: "day_period",
|
|
103
|
+
35: "day_period",
|
|
104
|
+
36: "day_period",
|
|
118
105
|
icu.DateFormat.HOUR0_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
119
106
|
icu.DateFormat.HOUR1_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
120
107
|
icu.DateFormat.HOUR_OF_DAY0_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
@@ -146,8 +133,8 @@ _SOURCE_TO_JSON_MAP: dict[str, str] = {
|
|
|
146
133
|
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
|
147
134
|
class DateTimeFormatOptions:
|
|
148
135
|
locale_matcher: LocaleMatcherT = "best fit"
|
|
149
|
-
hour12: Hour12T = None
|
|
150
|
-
hour_cycle: HourCycleT = None
|
|
136
|
+
hour12: Hour12T | None = None
|
|
137
|
+
hour_cycle: HourCycleT | None = None
|
|
151
138
|
|
|
152
139
|
era: EraFormatT | None = None
|
|
153
140
|
year: YearFormatT | None = None
|
|
@@ -165,6 +152,10 @@ class DateTimeFormatOptions:
|
|
|
165
152
|
return {
|
|
166
153
|
k: v
|
|
167
154
|
for k, v in (
|
|
155
|
+
("localeMatcher", self.locale_matcher),
|
|
156
|
+
("hour12", self.hour12),
|
|
157
|
+
("hourCycle", self.hour_cycle),
|
|
158
|
+
|
|
168
159
|
("era", self.era),
|
|
169
160
|
("year", self.year),
|
|
170
161
|
("month", self.month),
|
|
@@ -323,7 +314,7 @@ class FormatPatternNotFoundException(Exception):
|
|
|
323
314
|
|
|
324
315
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
325
316
|
class DateTimePatternPart:
|
|
326
|
-
type:
|
|
317
|
+
type: DatetimePatternPartTypeT
|
|
327
318
|
value: str
|
|
328
319
|
_pattern: str | None = None
|
|
329
320
|
|
|
@@ -337,7 +328,7 @@ class DateTimePatternPart:
|
|
|
337
328
|
|
|
338
329
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
339
330
|
class DateTimeIntervalPatternPart:
|
|
340
|
-
type:
|
|
331
|
+
type: DatetimePatternPartTypeT
|
|
341
332
|
value: str
|
|
342
333
|
source: Literal["start_range", "end_range", "shared"]
|
|
343
334
|
|
|
@@ -398,15 +389,18 @@ class _PartSpan:
|
|
|
398
389
|
|
|
399
390
|
|
|
400
391
|
class DateTimeFormat:
|
|
401
|
-
locale:
|
|
392
|
+
locale: Locale
|
|
402
393
|
options: DateTimeFormatOptions
|
|
403
394
|
|
|
404
395
|
def __init__(
|
|
405
396
|
self,
|
|
406
|
-
locale: str,
|
|
397
|
+
locale: Locale | str,
|
|
407
398
|
options: DateTimeFormatOptions | DateTimeFormatOptionsDictT | None = None,
|
|
408
399
|
) -> None:
|
|
409
|
-
|
|
400
|
+
if isinstance(locale, Locale):
|
|
401
|
+
self.locale = locale
|
|
402
|
+
else:
|
|
403
|
+
self.locale = Locale(locale)
|
|
410
404
|
if options is None:
|
|
411
405
|
self.options = DateTimeFormatOptions()
|
|
412
406
|
elif isinstance(options, DateTimeFormatOptions):
|
|
@@ -414,13 +408,9 @@ class DateTimeFormat:
|
|
|
414
408
|
else:
|
|
415
409
|
self.options = DateTimeFormatOptions(**options)
|
|
416
410
|
|
|
417
|
-
@cached_property
|
|
418
|
-
def _icu_locale(self) -> icu.Locale: # ty: ignore[unresolved-attribute]
|
|
419
|
-
return icu.Locale(self.locale) # ty: ignore[unresolved-attribute]
|
|
420
|
-
|
|
421
411
|
@cached_property
|
|
422
412
|
def _matched_pattern(self) -> _MatchedFormatPattern:
|
|
423
|
-
return _options_to_format_pattern(self._icu_locale, self.options)
|
|
413
|
+
return _options_to_format_pattern(self.locale._icu_locale, self.options)
|
|
424
414
|
|
|
425
415
|
@cached_property
|
|
426
416
|
def _icu_pattern(self) -> str:
|
|
@@ -428,7 +418,7 @@ class DateTimeFormat:
|
|
|
428
418
|
|
|
429
419
|
@cached_property
|
|
430
420
|
def _icu_date_format(self) -> icu.SimpleDateFormat: # ty: ignore[unresolved-attribute]
|
|
431
|
-
return icu.SimpleDateFormat(self._icu_pattern, self._icu_locale) # ty: ignore[unresolved-attribute]
|
|
421
|
+
return icu.SimpleDateFormat(self._icu_pattern, self.locale._icu_locale) # ty: ignore[unresolved-attribute]
|
|
432
422
|
|
|
433
423
|
def format(self, datetime_: dt.datetime, /) -> str:
|
|
434
424
|
return self._icu_date_format.format(datetime_)
|
|
@@ -448,7 +438,7 @@ class DateTimeFormat:
|
|
|
448
438
|
if char != prev_char and count > 0:
|
|
449
439
|
yield DateTimePatternPart(
|
|
450
440
|
type=_PATTERN_SYMBOL_TO_TYPE.get(prev_char, "unknown"),
|
|
451
|
-
value=icu.SimpleDateFormat(prev_char * count, self._icu_locale).format(datetime_), # ty: ignore[unresolved-attribute]
|
|
441
|
+
value=icu.SimpleDateFormat(prev_char * count, self.locale._icu_locale).format(datetime_), # ty: ignore[unresolved-attribute]
|
|
452
442
|
_pattern=prev_char * count,
|
|
453
443
|
)
|
|
454
444
|
count = 0
|
|
@@ -476,7 +466,7 @@ class DateTimeFormat:
|
|
|
476
466
|
if count > 0:
|
|
477
467
|
yield DateTimePatternPart(
|
|
478
468
|
type=_PATTERN_SYMBOL_TO_TYPE.get(prev_char, "unknown"),
|
|
479
|
-
value=icu.SimpleDateFormat(prev_char * count, self._icu_locale).format(datetime_), # ty: ignore[unresolved-attribute]
|
|
469
|
+
value=icu.SimpleDateFormat(prev_char * count, self.locale._icu_locale).format(datetime_), # ty: ignore[unresolved-attribute]
|
|
480
470
|
_pattern=prev_char * count,
|
|
481
471
|
)
|
|
482
472
|
assert not literal_chars # noqa: S101
|
|
@@ -489,7 +479,7 @@ class DateTimeFormat:
|
|
|
489
479
|
@cached_property
|
|
490
480
|
def _icu_dateinterval_format(self) -> icu.DateIntervalFormat: # ty: ignore[unresolved-attribute]
|
|
491
481
|
possible_skeletons = list(_options_to_possible_skeletons(self.options))
|
|
492
|
-
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self._icu_locale) # ty: ignore[unresolved-attribute]
|
|
482
|
+
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self.locale._icu_locale) # ty: ignore[unresolved-attribute]
|
|
493
483
|
|
|
494
484
|
def format_range(
|
|
495
485
|
self,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
|
|
5
|
+
import icu # type: ignore[import-untyped]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Locale:
|
|
9
|
+
tag: str
|
|
10
|
+
|
|
11
|
+
def __init__(self, tag: str) -> None:
|
|
12
|
+
self.tag = tag
|
|
13
|
+
|
|
14
|
+
@cached_property
|
|
15
|
+
def _icu_locale(self) -> icu.Locale: # ty: ignore[unresolved-attribute]
|
|
16
|
+
return icu.Locale(self.tag) # ty: ignore[unresolved-attribute]
|
|
File without changes
|
|
File without changes
|