python-intl 0.7.1__tar.gz → 0.9.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.7.1 → python_intl-0.9.0}/PKG-INFO +62 -2
- {python_intl-0.7.1 → python_intl-0.9.0}/README.md +61 -1
- {python_intl-0.7.1 → python_intl-0.9.0}/pyproject.toml +62 -32
- python_intl-0.9.0/pyproject.toml.orig +131 -0
- {python_intl-0.7.1 → python_intl-0.9.0}/python_intl/__init__.py +6 -1
- python_intl-0.9.0/python_intl/_types.py +94 -0
- {python_intl-0.7.1 → python_intl-0.9.0}/python_intl/collator.py +29 -37
- {python_intl-0.7.1 → python_intl-0.9.0}/python_intl/datetimeformat.py +77 -85
- python_intl-0.9.0/python_intl/locale.py +16 -0
- python_intl-0.9.0/python_intl/numberformat.py +191 -0
- python_intl-0.7.1/pyproject.toml.orig +0 -61
- python_intl-0.7.1/python_intl/_types.py +0 -3
- {python_intl-0.7.1 → python_intl-0.9.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.9.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,55 @@ 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.Locale("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
|
+
|
|
74
|
+
### `Intl.NumberFormat`
|
|
75
|
+
|
|
76
|
+
#### Example usage
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import python_intl as Intl
|
|
80
|
+
|
|
81
|
+
# Format liters
|
|
82
|
+
number = 1234.567
|
|
83
|
+
formatter = Intl.NumberFormat("de-DE", {"style": "unit", "unit": "liter"})
|
|
84
|
+
formatter.format(number)
|
|
85
|
+
# Result = "1.234,567 l"
|
|
86
|
+
|
|
87
|
+
# Format euros
|
|
88
|
+
number = 1234.567
|
|
89
|
+
formatter = Intl.NumberFormat("de-DE", {"style": "currency", "currency": "EUR"})
|
|
90
|
+
formatter.format(number)
|
|
91
|
+
# Result = "1.234,57 €"
|
|
92
|
+
# Note: Result will be automatically rounded to two decimal places, as EUR defines.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Compatibility
|
|
96
|
+
|
|
97
|
+
| Method | Status | Python name |
|
|
98
|
+
| --------------------------------- | :----: | ------------------------------------ |
|
|
99
|
+
| `NumberFormat.format` | ✅ | |
|
|
100
|
+
| `NumberFormat.formatToParts` | ❌ | `NumberFormat.format_to_parts` |
|
|
101
|
+
| `NumberFormat.supportedLocalesOf` | ❌ | |
|
|
102
|
+
| `NumberFormat.formatRange` | ✅ | `NumberFormat.format_range` |
|
|
103
|
+
| `NumberFormat.formatRangeToParts` | ❌ | `NumberFormat.format_range_to_parts` |
|
|
104
|
+
| `NumberFormat.resolvedOptions` | ❌ | |
|
|
105
|
+
|
|
57
106
|
### `Intl.DateTimeFormat`
|
|
58
107
|
|
|
59
108
|
#### Example usage
|
|
@@ -121,7 +170,18 @@ Be sure to be able to install `PyICU`, see the installation docs there:
|
|
|
121
170
|
https://gitlab.pyicu.org/main/pyicu#installing-pyicu
|
|
122
171
|
|
|
123
172
|
**Hint:** I mainly did run into issues with `pkg-config` not finding the ICU library,
|
|
124
|
-
setting `PKG_CONFIG_PATH` accordingly helps most of the time I guess.
|
|
173
|
+
setting `PKG_CONFIG_PATH` accordingly helps most of the time I guess. See [`.envrc`](.envrc)
|
|
174
|
+
for how to set this up correctly on `nix` based systems.
|
|
125
175
|
|
|
126
176
|
When this is done you should be able to install `python-intl` using any package
|
|
127
177
|
manager, like `pip install python-intl` or `uv add python-intl`.
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
If you want to contribute to this project, feel free to just fork the project,
|
|
182
|
+
create a dev branch in your fork and then create a pull request (PR). If you
|
|
183
|
+
are unsure about whether your changes really suits the project please create an
|
|
184
|
+
issue first, to talk about this.
|
|
185
|
+
|
|
186
|
+
Please do not contribute AI generated code unless we explicitly talked about
|
|
187
|
+
this and agreed upon doing so first. In general I do not want AI contributions.
|
|
@@ -41,6 +41,55 @@ 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.Locale("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
|
+
|
|
61
|
+
### `Intl.NumberFormat`
|
|
62
|
+
|
|
63
|
+
#### Example usage
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import python_intl as Intl
|
|
67
|
+
|
|
68
|
+
# Format liters
|
|
69
|
+
number = 1234.567
|
|
70
|
+
formatter = Intl.NumberFormat("de-DE", {"style": "unit", "unit": "liter"})
|
|
71
|
+
formatter.format(number)
|
|
72
|
+
# Result = "1.234,567 l"
|
|
73
|
+
|
|
74
|
+
# Format euros
|
|
75
|
+
number = 1234.567
|
|
76
|
+
formatter = Intl.NumberFormat("de-DE", {"style": "currency", "currency": "EUR"})
|
|
77
|
+
formatter.format(number)
|
|
78
|
+
# Result = "1.234,57 €"
|
|
79
|
+
# Note: Result will be automatically rounded to two decimal places, as EUR defines.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Compatibility
|
|
83
|
+
|
|
84
|
+
| Method | Status | Python name |
|
|
85
|
+
| --------------------------------- | :----: | ------------------------------------ |
|
|
86
|
+
| `NumberFormat.format` | ✅ | |
|
|
87
|
+
| `NumberFormat.formatToParts` | ❌ | `NumberFormat.format_to_parts` |
|
|
88
|
+
| `NumberFormat.supportedLocalesOf` | ❌ | |
|
|
89
|
+
| `NumberFormat.formatRange` | ✅ | `NumberFormat.format_range` |
|
|
90
|
+
| `NumberFormat.formatRangeToParts` | ❌ | `NumberFormat.format_range_to_parts` |
|
|
91
|
+
| `NumberFormat.resolvedOptions` | ❌ | |
|
|
92
|
+
|
|
44
93
|
### `Intl.DateTimeFormat`
|
|
45
94
|
|
|
46
95
|
#### Example usage
|
|
@@ -108,7 +157,18 @@ Be sure to be able to install `PyICU`, see the installation docs there:
|
|
|
108
157
|
https://gitlab.pyicu.org/main/pyicu#installing-pyicu
|
|
109
158
|
|
|
110
159
|
**Hint:** I mainly did run into issues with `pkg-config` not finding the ICU library,
|
|
111
|
-
setting `PKG_CONFIG_PATH` accordingly helps most of the time I guess.
|
|
160
|
+
setting `PKG_CONFIG_PATH` accordingly helps most of the time I guess. See [`.envrc`](.envrc)
|
|
161
|
+
for how to set this up correctly on `nix` based systems.
|
|
112
162
|
|
|
113
163
|
When this is done you should be able to install `python-intl` using any package
|
|
114
164
|
manager, like `pip install python-intl` or `uv add python-intl`.
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
If you want to contribute to this project, feel free to just fork the project,
|
|
169
|
+
create a dev branch in your fork and then create a pull request (PR). If you
|
|
170
|
+
are unsure about whether your changes really suits the project please create an
|
|
171
|
+
issue first, to talk about this.
|
|
172
|
+
|
|
173
|
+
Please do not contribute AI generated code unless we explicitly talked about
|
|
174
|
+
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.9.0"
|
|
4
4
|
description = "Python implementation of the Intl JavaScript API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -17,11 +17,14 @@ Repository = "https://github.com/ddanier/python-intl"
|
|
|
17
17
|
|
|
18
18
|
[dependency-groups]
|
|
19
19
|
dev = [
|
|
20
|
+
"basedpyright>=1.40.1",
|
|
20
21
|
"mypy>=2.3.1",
|
|
22
|
+
"pylsp-mypy>=0.8.0",
|
|
21
23
|
"pyright>=1.1.411",
|
|
22
24
|
"pytest>=9.1.1",
|
|
23
25
|
"pytest-cov>=7.1.0",
|
|
24
26
|
"pytest-xdist>=3.8.0",
|
|
27
|
+
"python-lsp-server>=1.15.0",
|
|
25
28
|
"ruff>=0.16.3",
|
|
26
29
|
"tox>=4.60.0",
|
|
27
30
|
"ty>=0.0.72",
|
|
@@ -35,59 +38,86 @@ markers = [
|
|
|
35
38
|
|
|
36
39
|
[tool.ruff]
|
|
37
40
|
line-length = 115
|
|
38
|
-
target-version = "py312"
|
|
39
41
|
output-format = "grouped"
|
|
40
42
|
|
|
41
43
|
[tool.ruff.lint]
|
|
42
44
|
select = [
|
|
43
|
-
"
|
|
45
|
+
"A",
|
|
46
|
+
"ANN",
|
|
47
|
+
"ARG",
|
|
48
|
+
"ASYNC",
|
|
49
|
+
"B",
|
|
50
|
+
"BLE",
|
|
51
|
+
"C4",
|
|
52
|
+
"COM",
|
|
53
|
+
"DTZ",
|
|
44
54
|
"E",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
55
|
+
"EM",
|
|
56
|
+
"EXE",
|
|
57
|
+
"F",
|
|
58
|
+
"FA",
|
|
59
|
+
"FBT",
|
|
60
|
+
"FURB",
|
|
47
61
|
"I",
|
|
62
|
+
"ICN",
|
|
63
|
+
"ISC",
|
|
48
64
|
"N",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
65
|
+
"PERF",
|
|
66
|
+
"PIE",
|
|
67
|
+
"PL",
|
|
68
|
+
"PT",
|
|
69
|
+
"PTH",
|
|
70
|
+
"Q",
|
|
71
|
+
"RSE",
|
|
72
|
+
"RUF",
|
|
51
73
|
"S",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"C4",
|
|
74
|
+
"SIM",
|
|
75
|
+
"SLOT",
|
|
76
|
+
"T10",
|
|
56
77
|
"T20",
|
|
57
|
-
"
|
|
58
|
-
"ARG",
|
|
78
|
+
"TC",
|
|
59
79
|
"TD",
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"A002",
|
|
65
|
-
"A003",
|
|
66
|
-
"ANN401",
|
|
67
|
-
"C901",
|
|
68
|
-
"N8",
|
|
69
|
-
"B008",
|
|
70
|
-
"F405",
|
|
71
|
-
"F821",
|
|
80
|
+
"TID",
|
|
81
|
+
"TRY",
|
|
82
|
+
"UP",
|
|
83
|
+
"W",
|
|
72
84
|
]
|
|
85
|
+
ignore = ["N812"]
|
|
73
86
|
|
|
74
87
|
[tool.ruff.lint.per-file-ignores]
|
|
75
|
-
"
|
|
76
|
-
"conftest.py" = [
|
|
88
|
+
"test_*.py" = [
|
|
77
89
|
"S101",
|
|
78
|
-
"
|
|
79
|
-
"F401",
|
|
90
|
+
"ANN201",
|
|
80
91
|
]
|
|
81
|
-
"
|
|
92
|
+
"conftest.py" = [
|
|
82
93
|
"S101",
|
|
83
|
-
"
|
|
84
|
-
"F401",
|
|
94
|
+
"ANN201",
|
|
85
95
|
]
|
|
86
96
|
|
|
87
97
|
[tool.ruff.lint.isort]
|
|
88
98
|
combine-as-imports = true
|
|
89
99
|
force-wrap-aliases = true
|
|
90
100
|
|
|
101
|
+
[tool.ty.analysis]
|
|
102
|
+
replace-imports-with-any = ["icu.**"]
|
|
103
|
+
|
|
104
|
+
[[tool.mypy.overrides]]
|
|
105
|
+
module = ["icu.*"]
|
|
106
|
+
ignore_missing_imports = true
|
|
107
|
+
|
|
108
|
+
[tool.basedpyright]
|
|
109
|
+
pythonVersion = "3.12"
|
|
110
|
+
reportAny = false
|
|
111
|
+
reportMatchNotExhaustive = false
|
|
112
|
+
reportUnknownLambdaType = false
|
|
113
|
+
reportPrivateUsage = false
|
|
114
|
+
allowedUntypedLibraries = ["icu"]
|
|
115
|
+
reportAttributeAccessIssue = false
|
|
116
|
+
reportUnknownVariableType = false
|
|
117
|
+
reportUnknownMemberType = false
|
|
118
|
+
reportUnknownParameterType = false
|
|
119
|
+
reportUnknownArgumentType = false
|
|
120
|
+
|
|
91
121
|
[tool.uv.build-backend]
|
|
92
122
|
module-name = "python_intl"
|
|
93
123
|
module-root = ""
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "python-intl"
|
|
3
|
+
version = "0.9.0"
|
|
4
|
+
description = "Python implementation of the Intl JavaScript API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "David Danier", email = "david.danier@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pyicu>=2.16.2",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Repository = "https://github.com/ddanier/python-intl"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"basedpyright>=1.40.1",
|
|
22
|
+
"mypy>=2.3.1",
|
|
23
|
+
"pylsp-mypy>=0.8.0",
|
|
24
|
+
"pyright>=1.1.411",
|
|
25
|
+
"pytest>=9.1.1",
|
|
26
|
+
"pytest-cov>=7.1.0",
|
|
27
|
+
"pytest-xdist>=3.8.0",
|
|
28
|
+
"python-lsp-server>=1.15.0",
|
|
29
|
+
"ruff>=0.16.3",
|
|
30
|
+
"tox>=4.60.0",
|
|
31
|
+
"ty>=0.0.72",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
markers = [
|
|
36
|
+
"unit: mark a test as a unit test",
|
|
37
|
+
"node: mark tests jun against the node/JS implementation",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 115
|
|
42
|
+
output-format = "grouped"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = [
|
|
46
|
+
"A", # flake8-builtins
|
|
47
|
+
"ANN", # flake8-annotations
|
|
48
|
+
"ARG", # flake8-unused-arguments
|
|
49
|
+
"ASYNC", # flake8-async
|
|
50
|
+
"B", # flake8-bugbear
|
|
51
|
+
"BLE", # flake8-blind-except
|
|
52
|
+
"C4", # flake8-comprehensions
|
|
53
|
+
"COM", # flake8-commas
|
|
54
|
+
"DTZ", # flake8-datetimez
|
|
55
|
+
"E", # pycodestyle errors
|
|
56
|
+
"EM", # flake8-errmsg
|
|
57
|
+
"EXE", # flake8-executable
|
|
58
|
+
"F", # pyflakes
|
|
59
|
+
"FA", # flake8-future-annotations
|
|
60
|
+
"FBT", # flake8-boolean-trap
|
|
61
|
+
"FURB", # refurb
|
|
62
|
+
"I", # isort
|
|
63
|
+
"ICN", # flake8-import-conventions
|
|
64
|
+
"ISC", # flake8-implicit-str-concat
|
|
65
|
+
"N", # pep8-naming
|
|
66
|
+
"PERF", # perflint
|
|
67
|
+
"PIE", # flake8-pie
|
|
68
|
+
"PL", # pylint
|
|
69
|
+
"PT", # flake8-pytest-style
|
|
70
|
+
"PTH", # flake8-use-pathlib
|
|
71
|
+
"Q", # flake8-quotes
|
|
72
|
+
"RSE", # flake8-raise
|
|
73
|
+
"RUF", # ruff-specific rules
|
|
74
|
+
"S", # flake8-bandit
|
|
75
|
+
"SIM", # flake8-simplify
|
|
76
|
+
"SLOT", # flake8-slots
|
|
77
|
+
"T10", # flake8-debugger
|
|
78
|
+
"T20", # flake8-print
|
|
79
|
+
"TC", # flake8-type-checking
|
|
80
|
+
"TD", # flake8-todos
|
|
81
|
+
"TID", # flake8-tidy-imports
|
|
82
|
+
"TRY", # tryceratops
|
|
83
|
+
"UP", # pyupgrade
|
|
84
|
+
"W", # pycodestyle warnings
|
|
85
|
+
]
|
|
86
|
+
ignore = [
|
|
87
|
+
"N812", # Allow using "import python_intl as Intl"
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.per-file-ignores]
|
|
91
|
+
"test_*.py" = [
|
|
92
|
+
"S101", # Allow usage of assert in tests
|
|
93
|
+
"ANN201", # Don't require to define return types in tests
|
|
94
|
+
]
|
|
95
|
+
"conftest.py" = [
|
|
96
|
+
"S101", # Allow usage of assert in tests
|
|
97
|
+
"ANN201", # Don't require to define return types in tests
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.isort]
|
|
101
|
+
combine-as-imports = true
|
|
102
|
+
force-wrap-aliases = true
|
|
103
|
+
|
|
104
|
+
[tool.ty.analysis]
|
|
105
|
+
replace-imports-with-any = ["icu.**"]
|
|
106
|
+
|
|
107
|
+
[[tool.mypy.overrides]]
|
|
108
|
+
module = ["icu.*"]
|
|
109
|
+
ignore_missing_imports = true
|
|
110
|
+
|
|
111
|
+
[tool.basedpyright]
|
|
112
|
+
pythonVersion = "3.12"
|
|
113
|
+
reportAny = false
|
|
114
|
+
reportMatchNotExhaustive = false
|
|
115
|
+
reportUnknownLambdaType = false
|
|
116
|
+
reportPrivateUsage = false
|
|
117
|
+
# The following are only necessary cause of untyped icu library
|
|
118
|
+
allowedUntypedLibraries = ["icu"]
|
|
119
|
+
reportAttributeAccessIssue = false
|
|
120
|
+
reportUnknownVariableType = false
|
|
121
|
+
reportUnknownMemberType = false
|
|
122
|
+
reportUnknownParameterType = false
|
|
123
|
+
reportUnknownArgumentType = false
|
|
124
|
+
|
|
125
|
+
[build-system]
|
|
126
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
127
|
+
build-backend = "uv_build"
|
|
128
|
+
|
|
129
|
+
[tool.uv.build-backend]
|
|
130
|
+
module-name = "python_intl"
|
|
131
|
+
module-root = ""
|
|
@@ -7,5 +7,10 @@ from .datetimeformat import (
|
|
|
7
7
|
DateTimeFormatOptions as DateTimeFormatOptions,
|
|
8
8
|
DateTimeIntervalPatternPart as DateTimeIntervalPatternPart,
|
|
9
9
|
DateTimePatternPart as DateTimePatternPart,
|
|
10
|
-
|
|
10
|
+
FormatPatternNotFoundError as FormatPatternNotFoundError,
|
|
11
|
+
)
|
|
12
|
+
from .numberformat import (
|
|
13
|
+
InvalidNumberFormatOptionError as InvalidNumberFormatOptionError,
|
|
14
|
+
NumberFormat as NumberFormat,
|
|
15
|
+
NumberFormatOptions as NumberFormatOptions,
|
|
11
16
|
)
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
# datetime
|
|
11
|
+
type EraFormatT = Literal["long", "short", "narrow"]
|
|
12
|
+
type YearFormatT = Literal["numeric", "2-digit"]
|
|
13
|
+
type MonthFormatT = Literal["numeric", "2-digit", "long", "short", "narrow"]
|
|
14
|
+
type WeekdayFormatT = Literal["long", "short", "narrow"]
|
|
15
|
+
type DayFormatT = Literal["numeric", "2-digit"]
|
|
16
|
+
type DayPeriodFormatT = Literal["long", "short", "narrow"]
|
|
17
|
+
type HourFormatT = Literal["numeric", "2-digit"]
|
|
18
|
+
type MinuteFormatT = Literal["numeric", "2-digit"]
|
|
19
|
+
type SecondFormatT = Literal["numeric", "2-digit"]
|
|
20
|
+
type FractionSecondDigitsFormatT = Literal[1, 2, 3]
|
|
21
|
+
type TimezoneNameFormatT = Literal[
|
|
22
|
+
"short",
|
|
23
|
+
"long",
|
|
24
|
+
"short_offset",
|
|
25
|
+
"long_offset",
|
|
26
|
+
"short_generic",
|
|
27
|
+
"long_generic",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# number
|
|
31
|
+
type StyleT = Literal["decimal", "currency", "percent", "unit"]
|
|
32
|
+
type CurrencyT = str # 3 char ISO code
|
|
33
|
+
type CurrencyDisplayT = Literal["code", "symbol", "narrow_symbol", "name"]
|
|
34
|
+
type CurrencySignT = Literal["standard", "accounting"]
|
|
35
|
+
type UnitT = Literal[
|
|
36
|
+
# see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf#supported_unit_identifiers
|
|
37
|
+
# for Intl reference
|
|
38
|
+
# see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classicu_1_1MeasureUnit.html
|
|
39
|
+
# for ICU reference
|
|
40
|
+
"acre",
|
|
41
|
+
"bit",
|
|
42
|
+
"byte",
|
|
43
|
+
"celsius",
|
|
44
|
+
"centimeter",
|
|
45
|
+
"day",
|
|
46
|
+
"degree",
|
|
47
|
+
"fahrenheit",
|
|
48
|
+
"fluid-ounce",
|
|
49
|
+
"foot",
|
|
50
|
+
"gallon",
|
|
51
|
+
"gigabit",
|
|
52
|
+
"gigabyte",
|
|
53
|
+
"gram",
|
|
54
|
+
"hectare",
|
|
55
|
+
"hour",
|
|
56
|
+
"inch",
|
|
57
|
+
"kilobit",
|
|
58
|
+
"kilobyte",
|
|
59
|
+
"kilogram",
|
|
60
|
+
"kilometer",
|
|
61
|
+
"liter",
|
|
62
|
+
"megabit",
|
|
63
|
+
"megabyte",
|
|
64
|
+
"meter",
|
|
65
|
+
"microsecond",
|
|
66
|
+
"mile",
|
|
67
|
+
"mile-scandinavian",
|
|
68
|
+
"milliliter",
|
|
69
|
+
"millimeter",
|
|
70
|
+
"millisecond",
|
|
71
|
+
"minute",
|
|
72
|
+
"month",
|
|
73
|
+
"nanosecond",
|
|
74
|
+
"ounce",
|
|
75
|
+
"percent",
|
|
76
|
+
"petabyte",
|
|
77
|
+
"pound",
|
|
78
|
+
"second",
|
|
79
|
+
"stone",
|
|
80
|
+
"terabit",
|
|
81
|
+
"terabyte",
|
|
82
|
+
"week",
|
|
83
|
+
"yard",
|
|
84
|
+
"year",
|
|
85
|
+
]
|
|
86
|
+
type UnitDisplayT = Literal["short", "narrow", "long"]
|
|
87
|
+
|
|
88
|
+
# collator
|
|
89
|
+
# type UsageT = Literal["sort", "search"]
|
|
90
|
+
# type CollationT = Literal["emoji", "pinyin", "stroke"]
|
|
91
|
+
type CaseFirstT = Literal["upper", "lower", "false"]
|
|
92
|
+
type SensitivityT = Literal["base", "accent", "case", "variant"]
|
|
93
|
+
|
|
94
|
+
type ComparisonResultT = Literal[-1, 0, 1]
|
|
@@ -2,25 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import functools
|
|
5
|
-
from collections.abc import Callable, Iterable
|
|
6
5
|
from functools import cached_property
|
|
7
|
-
from typing import TYPE_CHECKING,
|
|
6
|
+
from typing import TYPE_CHECKING, overload
|
|
8
7
|
|
|
9
|
-
import icu
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from typing import NotRequired, TypedDict
|
|
8
|
+
import icu
|
|
13
9
|
|
|
10
|
+
from .locale import Locale
|
|
14
11
|
|
|
15
12
|
if TYPE_CHECKING:
|
|
16
|
-
from .
|
|
17
|
-
|
|
18
|
-
# type UsageT = Literal["sort", "search"]
|
|
19
|
-
# type CollationT = Literal["emoji", "pinyin", "stroke"]
|
|
20
|
-
type CaseFirstT = Literal["upper", "lower", "false"]
|
|
21
|
-
type SensitivityT = Literal["base", "accent", "case", "variant"]
|
|
13
|
+
from collections.abc import Callable, Iterable
|
|
14
|
+
from typing import NotRequired, TypedDict
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
from ._types import CaseFirstT, ComparisonResultT, LocaleMatcherT, SensitivityT
|
|
24
17
|
|
|
25
18
|
# Important: Must be the same as CollatorOptions
|
|
26
19
|
# (nothing is required, as this will be used to construct a
|
|
@@ -61,23 +54,26 @@ class CollatorOptions:
|
|
|
61
54
|
}
|
|
62
55
|
|
|
63
56
|
|
|
64
|
-
_COLLATOR_RESULT_TO_RESULT: dict[icu.UCollationResult, ComparisonResultT] = {
|
|
65
|
-
icu.UCollationResult.LESS: -1,
|
|
66
|
-
icu.UCollationResult.EQUAL: 0,
|
|
67
|
-
icu.UCollationResult.GREATER: 1,
|
|
57
|
+
_COLLATOR_RESULT_TO_RESULT: dict[icu.UCollationResult, ComparisonResultT] = {
|
|
58
|
+
icu.UCollationResult.LESS: -1,
|
|
59
|
+
icu.UCollationResult.EQUAL: 0,
|
|
60
|
+
icu.UCollationResult.GREATER: 1,
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
|
|
71
64
|
class Collator:
|
|
72
|
-
locale:
|
|
65
|
+
locale: Locale
|
|
73
66
|
options: CollatorOptions
|
|
74
67
|
|
|
75
68
|
def __init__(
|
|
76
69
|
self,
|
|
77
|
-
locale: str,
|
|
70
|
+
locale: Locale | str,
|
|
78
71
|
options: CollatorOptions | CollatorOptionsDictT | None = None,
|
|
79
72
|
) -> None:
|
|
80
|
-
|
|
73
|
+
if isinstance(locale, Locale):
|
|
74
|
+
self.locale = locale
|
|
75
|
+
else:
|
|
76
|
+
self.locale = Locale(locale)
|
|
81
77
|
if options is None:
|
|
82
78
|
self.options = CollatorOptions()
|
|
83
79
|
elif isinstance(options, CollatorOptions):
|
|
@@ -86,41 +82,37 @@ class Collator:
|
|
|
86
82
|
self.options = CollatorOptions(**options)
|
|
87
83
|
|
|
88
84
|
@cached_property
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@cached_property
|
|
93
|
-
def _icu_collator(self) -> icu.Collator: # ty: ignore[unresolved-attribute]
|
|
94
|
-
collator = icu.Collator.createInstance(self._icu_locale) # ty: ignore[unresolved-attribute]
|
|
85
|
+
def _icu_collator(self) -> icu.Collator:
|
|
86
|
+
collator = icu.Collator.createInstance(self.locale._icu_locale)
|
|
95
87
|
|
|
96
88
|
if self.options.numeric:
|
|
97
|
-
collator.setAttribute(icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON)
|
|
89
|
+
collator.setAttribute(icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON)
|
|
98
90
|
|
|
99
91
|
if (
|
|
100
92
|
self.options.ignore_punctuation
|
|
101
93
|
or (
|
|
102
94
|
self.options.ignore_punctuation is None
|
|
103
|
-
and self._icu_locale.getLanguage() == "th"
|
|
95
|
+
and self.locale._icu_locale.getLanguage() == "th"
|
|
104
96
|
)
|
|
105
97
|
):
|
|
106
|
-
collator.setAttribute(icu.UCollAttribute.ALTERNATE_HANDLING, icu.UCollAttributeValue.SHIFTED)
|
|
98
|
+
collator.setAttribute(icu.UCollAttribute.ALTERNATE_HANDLING, icu.UCollAttributeValue.SHIFTED)
|
|
107
99
|
|
|
108
100
|
match self.options.case_first:
|
|
109
101
|
case "upper":
|
|
110
|
-
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.UPPER_FIRST)
|
|
102
|
+
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.UPPER_FIRST)
|
|
111
103
|
case "lower":
|
|
112
|
-
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.LOWER_FIRST)
|
|
104
|
+
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.LOWER_FIRST)
|
|
113
105
|
|
|
114
|
-
collator.setAttribute(icu.UCollAttribute.NORMALIZATION_MODE, icu.UCollAttributeValue.ON)
|
|
106
|
+
collator.setAttribute(icu.UCollAttribute.NORMALIZATION_MODE, icu.UCollAttributeValue.ON)
|
|
115
107
|
match self.options.sensitivity:
|
|
116
108
|
case "base":
|
|
117
|
-
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.PRIMARY)
|
|
109
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.PRIMARY)
|
|
118
110
|
case "accent":
|
|
119
|
-
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.SECONDARY)
|
|
111
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.SECONDARY)
|
|
120
112
|
case "case":
|
|
121
|
-
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.TERTIARY)
|
|
113
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.TERTIARY)
|
|
122
114
|
case "variant":
|
|
123
|
-
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.QUATERNARY)
|
|
115
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.QUATERNARY)
|
|
124
116
|
|
|
125
117
|
return collator
|
|
126
118
|
|
|
@@ -131,7 +123,7 @@ class Collator:
|
|
|
131
123
|
def sorted(self, items: Iterable[str], /, key: None = None) -> Iterable[str]: ...
|
|
132
124
|
@overload
|
|
133
125
|
def sorted[T](self, items: Iterable[T], /, key: Callable[[T], str]) -> Iterable[T]: ...
|
|
134
|
-
def sorted(self, items, /, key = None):
|
|
126
|
+
def sorted(self, items, /, key = None): # pyright: ignore[reportInconsistentOverload]
|
|
135
127
|
return sorted(
|
|
136
128
|
items,
|
|
137
129
|
key=functools.cmp_to_key(
|
|
@@ -1,56 +1,36 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
-
import datetime as dt
|
|
5
4
|
from functools import cache, cached_property
|
|
6
|
-
from typing import TYPE_CHECKING, Literal
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, override
|
|
7
6
|
|
|
8
|
-
import icu
|
|
7
|
+
import icu
|
|
8
|
+
|
|
9
|
+
from .locale import Locale
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
12
|
+
import datetime as dt
|
|
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,21 +89,27 @@ _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,
|
|
112
|
-
icu.DateFormat.ERA_FIELD: "era",
|
|
113
|
-
icu.DateFormat.YEAR_FIELD: "year",
|
|
114
|
-
icu.DateFormat.MONTH_FIELD: "month",
|
|
115
|
-
icu.DateFormat.DAY_OF_WEEK_FIELD: "weekday",
|
|
116
|
-
icu.DateFormat.DATE_FIELD: "day",
|
|
117
|
-
icu.DateFormat.AM_PM_FIELD: "day_period",
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
icu.DateFormat.
|
|
121
|
-
icu.DateFormat.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
icu.DateFormat.
|
|
125
|
-
icu.DateFormat.
|
|
92
|
+
_PATTERN_FIELD_TO_TYPE: dict[icu.UDateTimePatternField, DatetimePatternPartTypeT] = {
|
|
93
|
+
icu.DateFormat.ERA_FIELD: "era",
|
|
94
|
+
icu.DateFormat.YEAR_FIELD: "year",
|
|
95
|
+
icu.DateFormat.MONTH_FIELD: "month",
|
|
96
|
+
icu.DateFormat.DAY_OF_WEEK_FIELD: "weekday",
|
|
97
|
+
icu.DateFormat.DATE_FIELD: "day",
|
|
98
|
+
icu.DateFormat.AM_PM_FIELD: "day_period",
|
|
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",
|
|
105
|
+
icu.DateFormat.HOUR0_FIELD: "hour",
|
|
106
|
+
icu.DateFormat.HOUR1_FIELD: "hour",
|
|
107
|
+
icu.DateFormat.HOUR_OF_DAY0_FIELD: "hour",
|
|
108
|
+
icu.DateFormat.HOUR_OF_DAY1_FIELD: "hour",
|
|
109
|
+
icu.DateFormat.MINUTE_FIELD: "minute",
|
|
110
|
+
icu.DateFormat.SECOND_FIELD: "second",
|
|
111
|
+
icu.DateFormat.MILLISECOND_FIELD: "fraction_second_digits",
|
|
112
|
+
icu.DateFormat.TIMEZONE_FIELD: "time_zone_name",
|
|
126
113
|
}
|
|
127
114
|
_PATTERN_QUOTE = "'"
|
|
128
115
|
|
|
@@ -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),
|
|
@@ -181,7 +172,7 @@ class DateTimeFormatOptions:
|
|
|
181
172
|
}
|
|
182
173
|
|
|
183
174
|
|
|
184
|
-
def _options_to_possible_skeletons(options: DateTimeFormatOptions) -> Iterable[str]:
|
|
175
|
+
def _options_to_possible_skeletons(options: DateTimeFormatOptions) -> Iterable[str]: # noqa: PLR0912, PLR0915
|
|
185
176
|
skeleton_parts: list[str | tuple[str, ...]] = []
|
|
186
177
|
|
|
187
178
|
# Note: The parts should be ordered from big to small.
|
|
@@ -313,17 +304,18 @@ class _MatchedFormatPattern:
|
|
|
313
304
|
skeleton: str
|
|
314
305
|
pattern: str
|
|
315
306
|
|
|
307
|
+
@override
|
|
316
308
|
def __str__(self) -> str:
|
|
317
309
|
return self.pattern
|
|
318
310
|
|
|
319
311
|
|
|
320
|
-
class
|
|
312
|
+
class FormatPatternNotFoundError(Exception):
|
|
321
313
|
pass
|
|
322
314
|
|
|
323
315
|
|
|
324
316
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
325
317
|
class DateTimePatternPart:
|
|
326
|
-
type:
|
|
318
|
+
type: DatetimePatternPartTypeT
|
|
327
319
|
value: str
|
|
328
320
|
_pattern: str | None = None
|
|
329
321
|
|
|
@@ -337,7 +329,7 @@ class DateTimePatternPart:
|
|
|
337
329
|
|
|
338
330
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
339
331
|
class DateTimeIntervalPatternPart:
|
|
340
|
-
type:
|
|
332
|
+
type: DatetimePatternPartTypeT
|
|
341
333
|
value: str
|
|
342
334
|
source: Literal["start_range", "end_range", "shared"]
|
|
343
335
|
|
|
@@ -351,12 +343,12 @@ class DateTimeIntervalPatternPart:
|
|
|
351
343
|
|
|
352
344
|
@cache
|
|
353
345
|
def _options_to_format_pattern(
|
|
354
|
-
locale: icu.Locale,
|
|
346
|
+
locale: icu.Locale,
|
|
355
347
|
options: DateTimeFormatOptions,
|
|
356
348
|
) -> _MatchedFormatPattern:
|
|
357
349
|
possible_skeletons = list(_options_to_possible_skeletons(options))
|
|
358
350
|
|
|
359
|
-
generator = icu.DateTimePatternGenerator.createInstance(locale)
|
|
351
|
+
generator = icu.DateTimePatternGenerator.createInstance(locale)
|
|
360
352
|
|
|
361
353
|
# Try a perfect match
|
|
362
354
|
for skeleton in possible_skeletons:
|
|
@@ -377,7 +369,8 @@ def _options_to_format_pattern(
|
|
|
377
369
|
pattern=pattern,
|
|
378
370
|
)
|
|
379
371
|
|
|
380
|
-
|
|
372
|
+
error = "Didn't find pattern for desired options"
|
|
373
|
+
raise FormatPatternNotFoundError(error)
|
|
381
374
|
|
|
382
375
|
|
|
383
376
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
@@ -390,7 +383,7 @@ class _PartSpan:
|
|
|
390
383
|
return cls(start=0, end=0)
|
|
391
384
|
|
|
392
385
|
@classmethod
|
|
393
|
-
def from_constrained_fieldposition(cls, position: icu.ConstrainedFieldPosition) -> _PartSpan:
|
|
386
|
+
def from_constrained_fieldposition(cls, position: icu.ConstrainedFieldPosition) -> _PartSpan:
|
|
394
387
|
return cls(start=position.getStart(), end=position.getLimit())
|
|
395
388
|
|
|
396
389
|
def __contains__(self, inner: _PartSpan) -> bool:
|
|
@@ -398,15 +391,18 @@ class _PartSpan:
|
|
|
398
391
|
|
|
399
392
|
|
|
400
393
|
class DateTimeFormat:
|
|
401
|
-
locale:
|
|
394
|
+
locale: Locale
|
|
402
395
|
options: DateTimeFormatOptions
|
|
403
396
|
|
|
404
397
|
def __init__(
|
|
405
398
|
self,
|
|
406
|
-
locale: str,
|
|
399
|
+
locale: Locale | str,
|
|
407
400
|
options: DateTimeFormatOptions | DateTimeFormatOptionsDictT | None = None,
|
|
408
401
|
) -> None:
|
|
409
|
-
|
|
402
|
+
if isinstance(locale, Locale):
|
|
403
|
+
self.locale = locale
|
|
404
|
+
else:
|
|
405
|
+
self.locale = Locale(locale)
|
|
410
406
|
if options is None:
|
|
411
407
|
self.options = DateTimeFormatOptions()
|
|
412
408
|
elif isinstance(options, DateTimeFormatOptions):
|
|
@@ -414,21 +410,17 @@ class DateTimeFormat:
|
|
|
414
410
|
else:
|
|
415
411
|
self.options = DateTimeFormatOptions(**options)
|
|
416
412
|
|
|
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
413
|
@cached_property
|
|
422
414
|
def _matched_pattern(self) -> _MatchedFormatPattern:
|
|
423
|
-
return _options_to_format_pattern(self._icu_locale, self.options)
|
|
415
|
+
return _options_to_format_pattern(self.locale._icu_locale, self.options)
|
|
424
416
|
|
|
425
417
|
@cached_property
|
|
426
418
|
def _icu_pattern(self) -> str:
|
|
427
419
|
return self._matched_pattern.pattern
|
|
428
420
|
|
|
429
421
|
@cached_property
|
|
430
|
-
def _icu_date_format(self) -> icu.SimpleDateFormat:
|
|
431
|
-
return icu.SimpleDateFormat(self._icu_pattern, self._icu_locale)
|
|
422
|
+
def _icu_date_format(self) -> icu.SimpleDateFormat:
|
|
423
|
+
return icu.SimpleDateFormat(self._icu_pattern, self.locale._icu_locale)
|
|
432
424
|
|
|
433
425
|
def format(self, datetime_: dt.datetime, /) -> str:
|
|
434
426
|
return self._icu_date_format.format(datetime_)
|
|
@@ -448,7 +440,7 @@ class DateTimeFormat:
|
|
|
448
440
|
if char != prev_char and count > 0:
|
|
449
441
|
yield DateTimePatternPart(
|
|
450
442
|
type=_PATTERN_SYMBOL_TO_TYPE.get(prev_char, "unknown"),
|
|
451
|
-
value=icu.SimpleDateFormat(prev_char * count, self._icu_locale).format(datetime_),
|
|
443
|
+
value=icu.SimpleDateFormat(prev_char * count, self.locale._icu_locale).format(datetime_),
|
|
452
444
|
_pattern=prev_char * count,
|
|
453
445
|
)
|
|
454
446
|
count = 0
|
|
@@ -476,7 +468,7 @@ class DateTimeFormat:
|
|
|
476
468
|
if count > 0:
|
|
477
469
|
yield DateTimePatternPart(
|
|
478
470
|
type=_PATTERN_SYMBOL_TO_TYPE.get(prev_char, "unknown"),
|
|
479
|
-
value=icu.SimpleDateFormat(prev_char * count, self._icu_locale).format(datetime_),
|
|
471
|
+
value=icu.SimpleDateFormat(prev_char * count, self.locale._icu_locale).format(datetime_),
|
|
480
472
|
_pattern=prev_char * count,
|
|
481
473
|
)
|
|
482
474
|
assert not literal_chars # noqa: S101
|
|
@@ -487,16 +479,16 @@ class DateTimeFormat:
|
|
|
487
479
|
)
|
|
488
480
|
|
|
489
481
|
@cached_property
|
|
490
|
-
def _icu_dateinterval_format(self) -> icu.DateIntervalFormat:
|
|
482
|
+
def _icu_dateinterval_format(self) -> icu.DateIntervalFormat:
|
|
491
483
|
possible_skeletons = list(_options_to_possible_skeletons(self.options))
|
|
492
|
-
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self._icu_locale)
|
|
484
|
+
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self.locale._icu_locale)
|
|
493
485
|
|
|
494
486
|
def format_range(
|
|
495
487
|
self,
|
|
496
488
|
start_datetime: dt.datetime,
|
|
497
489
|
end_datetime: dt.datetime,
|
|
498
490
|
) -> str:
|
|
499
|
-
icu_date_interval = icu.DateInterval(start_datetime, end_datetime)
|
|
491
|
+
icu_date_interval = icu.DateInterval(start_datetime, end_datetime)
|
|
500
492
|
return self._icu_dateinterval_format.format(icu_date_interval)
|
|
501
493
|
|
|
502
494
|
def format_range_to_parts(
|
|
@@ -504,14 +496,14 @@ class DateTimeFormat:
|
|
|
504
496
|
start_datetime: dt.datetime,
|
|
505
497
|
end_datetime: dt.datetime,
|
|
506
498
|
) -> Iterable[DateTimeIntervalPatternPart]:
|
|
507
|
-
icu_date_interval = icu.DateInterval(start_datetime, end_datetime)
|
|
499
|
+
icu_date_interval = icu.DateInterval(start_datetime, end_datetime)
|
|
508
500
|
formatted = self._icu_dateinterval_format.formatToValue(icu_date_interval)
|
|
509
501
|
|
|
510
502
|
# Find spans of both datetimes (used to determine which parts have which source)
|
|
511
503
|
span_start = _PartSpan.empty()
|
|
512
504
|
span_end = _PartSpan.empty()
|
|
513
505
|
for part in formatted:
|
|
514
|
-
if part.getCategory() == icu.UFieldCategory.DATE_INTERVAL_SPAN:
|
|
506
|
+
if part.getCategory() == icu.UFieldCategory.DATE_INTERVAL_SPAN:
|
|
515
507
|
match part.getField():
|
|
516
508
|
case 0:
|
|
517
509
|
span_start = _PartSpan.from_constrained_fieldposition(part)
|
|
@@ -538,7 +530,7 @@ class DateTimeFormat:
|
|
|
538
530
|
source=source_of(_PartSpan(start=last_end, end=span.start)),
|
|
539
531
|
)
|
|
540
532
|
|
|
541
|
-
if part.getCategory() == icu.UFieldCategory.DATE:
|
|
533
|
+
if part.getCategory() == icu.UFieldCategory.DATE:
|
|
542
534
|
yield DateTimeIntervalPatternPart(
|
|
543
535
|
type=_PATTERN_FIELD_TO_TYPE.get(part.getField(), "unknown"),
|
|
544
536
|
value=result_string[span.start:span.end],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
|
|
5
|
+
import icu
|
|
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:
|
|
16
|
+
return icu.Locale(self.tag)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import decimal
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import icu
|
|
9
|
+
|
|
10
|
+
from .locale import Locale
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from typing import NotRequired, TypedDict
|
|
14
|
+
|
|
15
|
+
from ._types import (
|
|
16
|
+
CurrencyDisplayT,
|
|
17
|
+
CurrencySignT,
|
|
18
|
+
CurrencyT,
|
|
19
|
+
LocaleMatcherT,
|
|
20
|
+
StyleT,
|
|
21
|
+
UnitDisplayT,
|
|
22
|
+
UnitT,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
type NumberT = decimal.Decimal | float | int
|
|
26
|
+
|
|
27
|
+
# Important: Must be the same as DateTimeFormatOptions
|
|
28
|
+
# (nothing is required, as this will be used to construct a
|
|
29
|
+
# DateTimeFormatOptions instance, so default values apply then)
|
|
30
|
+
class NumberFormatOptionsDictT(TypedDict):
|
|
31
|
+
locale_matcher: NotRequired[LocaleMatcherT]
|
|
32
|
+
|
|
33
|
+
style: NotRequired[StyleT]
|
|
34
|
+
|
|
35
|
+
currency: NotRequired[CurrencyT]
|
|
36
|
+
currency_display: NotRequired[CurrencyDisplayT]
|
|
37
|
+
currency_sign: NotRequired[CurrencySignT]
|
|
38
|
+
|
|
39
|
+
unit: NotRequired[UnitT]
|
|
40
|
+
unit_display: NotRequired[UnitDisplayT]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_CURRENCY_DISPLAY_TO_JSON_MAP: dict[str, str] = {
|
|
44
|
+
"narrow_symbol": "narrowSymbol",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class InvalidNumberFormatOptionError(ValueError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
|
53
|
+
class NumberFormatOptions:
|
|
54
|
+
locale_matcher: LocaleMatcherT = "best fit"
|
|
55
|
+
|
|
56
|
+
style: StyleT = "decimal"
|
|
57
|
+
|
|
58
|
+
currency: CurrencyT | None = None
|
|
59
|
+
currency_display: CurrencyDisplayT = "symbol"
|
|
60
|
+
currency_sign: CurrencySignT = "standard"
|
|
61
|
+
|
|
62
|
+
unit: UnitT | None = None
|
|
63
|
+
unit_display: UnitDisplayT = "short"
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
error = None
|
|
67
|
+
if self.style == "currency" and self.currency is None:
|
|
68
|
+
error = "You need to provide a currency when using the currency style"
|
|
69
|
+
elif self.style == "unit" and self.unit is None:
|
|
70
|
+
error = "You need to provide a unit when using the unit style"
|
|
71
|
+
|
|
72
|
+
if error:
|
|
73
|
+
raise InvalidNumberFormatOptionError(error)
|
|
74
|
+
|
|
75
|
+
def to_json(self) -> dict[str, str | int]:
|
|
76
|
+
return {
|
|
77
|
+
k: v
|
|
78
|
+
for k, v in (
|
|
79
|
+
("localeMatcher", self.locale_matcher),
|
|
80
|
+
|
|
81
|
+
("style", self.style),
|
|
82
|
+
|
|
83
|
+
("currency", self.currency),
|
|
84
|
+
("currencyDisplay", _CURRENCY_DISPLAY_TO_JSON_MAP.get(
|
|
85
|
+
self.currency_display,
|
|
86
|
+
self.currency_display,
|
|
87
|
+
)),
|
|
88
|
+
("currencySign", self.currency_sign),
|
|
89
|
+
|
|
90
|
+
("unit", self.unit),
|
|
91
|
+
("unitDisplay", self.unit_display),
|
|
92
|
+
)
|
|
93
|
+
if v is not None
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _options_to_skeleton(options: NumberFormatOptions) -> str:
|
|
98
|
+
skeleton_parts: list[str] = []
|
|
99
|
+
|
|
100
|
+
match options.style:
|
|
101
|
+
case "decimal":
|
|
102
|
+
skeleton_parts.append("decimal-auto")
|
|
103
|
+
case "percent":
|
|
104
|
+
skeleton_parts.append("precision-integer")
|
|
105
|
+
skeleton_parts.append("scale/100")
|
|
106
|
+
skeleton_parts.append("percent")
|
|
107
|
+
case "currency":
|
|
108
|
+
skeleton_parts.append(f"currency/{options.currency}")
|
|
109
|
+
skeleton_parts.append("precision-currency-standard")
|
|
110
|
+
if options.currency_sign == "accounting":
|
|
111
|
+
skeleton_parts.append("sign-accounting")
|
|
112
|
+
match options.currency_display:
|
|
113
|
+
case "code":
|
|
114
|
+
skeleton_parts.append("unit-width-iso-code")
|
|
115
|
+
case "symbol":
|
|
116
|
+
skeleton_parts.append("unit-width-short")
|
|
117
|
+
case "narrow_symbol":
|
|
118
|
+
skeleton_parts.append("unit-width-narrow")
|
|
119
|
+
case "name":
|
|
120
|
+
skeleton_parts.append("unit-width-full-name")
|
|
121
|
+
case "unit":
|
|
122
|
+
skeleton_parts.append(f"unit/{options.unit}")
|
|
123
|
+
match options.unit_display:
|
|
124
|
+
case "short":
|
|
125
|
+
skeleton_parts.append("unit-width-short")
|
|
126
|
+
case "narrow":
|
|
127
|
+
skeleton_parts.append("unit-width-narrow")
|
|
128
|
+
case "long":
|
|
129
|
+
skeleton_parts.append("unit-width-full-name")
|
|
130
|
+
|
|
131
|
+
return " ".join(skeleton_parts)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class NumberFormat:
|
|
135
|
+
locale: Locale
|
|
136
|
+
options: NumberFormatOptions
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
locale: Locale | str,
|
|
141
|
+
options: NumberFormatOptions | NumberFormatOptionsDictT | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
if isinstance(locale, Locale):
|
|
144
|
+
self.locale = locale
|
|
145
|
+
else:
|
|
146
|
+
self.locale = Locale(locale)
|
|
147
|
+
if options is None:
|
|
148
|
+
self.options = NumberFormatOptions()
|
|
149
|
+
elif isinstance(options, NumberFormatOptions):
|
|
150
|
+
self.options = options
|
|
151
|
+
else:
|
|
152
|
+
self.options = NumberFormatOptions(**options)
|
|
153
|
+
|
|
154
|
+
@cached_property
|
|
155
|
+
def _icu_number_formatter(self) -> icu.LocalizedNumberFormatter:
|
|
156
|
+
skeleton = _options_to_skeleton(self.options)
|
|
157
|
+
return icu.NumberFormatter.forSkeleton(skeleton).locale(self.locale._icu_locale)
|
|
158
|
+
|
|
159
|
+
def format(self, value: NumberT, /) -> str:
|
|
160
|
+
match value:
|
|
161
|
+
case int():
|
|
162
|
+
return self._icu_number_formatter.formatInt(value)
|
|
163
|
+
case float():
|
|
164
|
+
return self._icu_number_formatter.formatDouble(value)
|
|
165
|
+
case decimal.Decimal():
|
|
166
|
+
return self._icu_number_formatter.formatDecimal(str(value).encode("ascii"))
|
|
167
|
+
|
|
168
|
+
@cached_property
|
|
169
|
+
def _icu_number_range_formatter(self) -> icu.LocalizedNumberRangeFormatter:
|
|
170
|
+
skeleton = _options_to_skeleton(self.options)
|
|
171
|
+
return (
|
|
172
|
+
icu.NumberRangeFormatter
|
|
173
|
+
.withLocale(self.locale._icu_locale)
|
|
174
|
+
.numberFormatterBoth(icu.NumberFormatter.forSkeleton(skeleton))
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def format_range(self, start_value: NumberT, end_value: NumberT, /) -> str:
|
|
178
|
+
match start_value, end_value:
|
|
179
|
+
case int(), int():
|
|
180
|
+
return self._icu_number_range_formatter.formatIntRange(start_value, end_value)
|
|
181
|
+
case float(), float():
|
|
182
|
+
return self._icu_number_range_formatter.formatDoubleRange(start_value, end_value)
|
|
183
|
+
case decimal.Decimal(), decimal.Decimal():
|
|
184
|
+
return self._icu_number_range_formatter.formatDoubleRange(
|
|
185
|
+
# For some reason formatDecimalRange fails, so we fall back to floats
|
|
186
|
+
float(start_value),
|
|
187
|
+
float(end_value),
|
|
188
|
+
)
|
|
189
|
+
case _:
|
|
190
|
+
error = "Both parameters passed to format_range must have the same type"
|
|
191
|
+
raise ValueError(error)
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "python-intl"
|
|
3
|
-
version = "0.7.1"
|
|
4
|
-
description = "Python implementation of the Intl JavaScript API"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
authors = [
|
|
7
|
-
{ name = "David Danier", email = "david.danier@gmail.com" }
|
|
8
|
-
]
|
|
9
|
-
license = "MIT"
|
|
10
|
-
license-files = ["LICENSE"]
|
|
11
|
-
requires-python = ">=3.12"
|
|
12
|
-
dependencies = [
|
|
13
|
-
"pyicu>=2.16.2",
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
[project.urls]
|
|
17
|
-
Repository = "https://github.com/ddanier/python-intl"
|
|
18
|
-
|
|
19
|
-
[dependency-groups]
|
|
20
|
-
dev = [
|
|
21
|
-
"mypy>=2.3.1",
|
|
22
|
-
"pyright>=1.1.411",
|
|
23
|
-
"pytest>=9.1.1",
|
|
24
|
-
"pytest-cov>=7.1.0",
|
|
25
|
-
"pytest-xdist>=3.8.0",
|
|
26
|
-
"ruff>=0.16.3",
|
|
27
|
-
"tox>=4.60.0",
|
|
28
|
-
"ty>=0.0.72",
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
[tool.pytest.ini_options]
|
|
32
|
-
markers = [
|
|
33
|
-
"unit: mark a test as a unit test",
|
|
34
|
-
"node: mark tests jun against the node/JS implementation",
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
[tool.ruff]
|
|
38
|
-
line-length = 115
|
|
39
|
-
target-version = "py312"
|
|
40
|
-
output-format = "grouped"
|
|
41
|
-
|
|
42
|
-
[tool.ruff.lint]
|
|
43
|
-
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"]
|
|
45
|
-
|
|
46
|
-
[tool.ruff.lint.per-file-ignores]
|
|
47
|
-
"__init__.py" = ["F401"]
|
|
48
|
-
"conftest.py" = ["S101","ANN","F401"]
|
|
49
|
-
"test_*.py" = ["S101","ANN","F401"]
|
|
50
|
-
|
|
51
|
-
[tool.ruff.lint.isort]
|
|
52
|
-
combine-as-imports = true
|
|
53
|
-
force-wrap-aliases = true
|
|
54
|
-
|
|
55
|
-
[build-system]
|
|
56
|
-
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
57
|
-
build-backend = "uv_build"
|
|
58
|
-
|
|
59
|
-
[tool.uv.build-backend]
|
|
60
|
-
module-name = "python_intl"
|
|
61
|
-
module-root = ""
|
|
File without changes
|