python-intl 0.8.0__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.8.0 → python_intl-0.9.0}/PKG-INFO +36 -3
- {python_intl-0.8.0 → python_intl-0.9.0}/README.md +35 -2
- {python_intl-0.8.0 → python_intl-0.9.0}/pyproject.toml +60 -33
- python_intl-0.9.0/pyproject.toml.orig +131 -0
- {python_intl-0.8.0 → 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.8.0 → python_intl-0.9.0}/python_intl/collator.py +18 -18
- {python_intl-0.8.0 → python_intl-0.9.0}/python_intl/datetimeformat.py +36 -34
- {python_intl-0.8.0 → python_intl-0.9.0}/python_intl/locale.py +3 -3
- python_intl-0.9.0/python_intl/numberformat.py +191 -0
- python_intl-0.8.0/pyproject.toml.orig +0 -63
- python_intl-0.8.0/python_intl/_types.py +0 -34
- {python_intl-0.8.0 → 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>
|
|
@@ -62,7 +62,7 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
62
62
|
import datetime as dt
|
|
63
63
|
import python_intl as Intl
|
|
64
64
|
|
|
65
|
-
locale = Intl.
|
|
65
|
+
locale = Intl.Locale("de-DE")
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
#### Compatibility
|
|
@@ -71,6 +71,38 @@ locale = Intl.DateTimeFormat("de-DE")
|
|
|
71
71
|
allow using the locale class instead of a string with the other classes. There will
|
|
72
72
|
be more later.
|
|
73
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
|
+
|
|
74
106
|
### `Intl.DateTimeFormat`
|
|
75
107
|
|
|
76
108
|
#### Example usage
|
|
@@ -138,7 +170,8 @@ Be sure to be able to install `PyICU`, see the installation docs there:
|
|
|
138
170
|
https://gitlab.pyicu.org/main/pyicu#installing-pyicu
|
|
139
171
|
|
|
140
172
|
**Hint:** I mainly did run into issues with `pkg-config` not finding the ICU library,
|
|
141
|
-
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.
|
|
142
175
|
|
|
143
176
|
When this is done you should be able to install `python-intl` using any package
|
|
144
177
|
manager, like `pip install python-intl` or `uv add python-intl`.
|
|
@@ -49,7 +49,7 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
49
49
|
import datetime as dt
|
|
50
50
|
import python_intl as Intl
|
|
51
51
|
|
|
52
|
-
locale = Intl.
|
|
52
|
+
locale = Intl.Locale("de-DE")
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
#### Compatibility
|
|
@@ -58,6 +58,38 @@ locale = Intl.DateTimeFormat("de-DE")
|
|
|
58
58
|
allow using the locale class instead of a string with the other classes. There will
|
|
59
59
|
be more later.
|
|
60
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
|
+
|
|
61
93
|
### `Intl.DateTimeFormat`
|
|
62
94
|
|
|
63
95
|
#### Example usage
|
|
@@ -125,7 +157,8 @@ Be sure to be able to install `PyICU`, see the installation docs there:
|
|
|
125
157
|
https://gitlab.pyicu.org/main/pyicu#installing-pyicu
|
|
126
158
|
|
|
127
159
|
**Hint:** I mainly did run into issues with `pkg-config` not finding the ICU library,
|
|
128
|
-
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.
|
|
129
162
|
|
|
130
163
|
When this is done you should be able to install `python-intl` using any package
|
|
131
164
|
manager, like `pip install python-intl` or `uv add python-intl`.
|
|
@@ -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",
|
|
@@ -39,57 +42,81 @@ output-format = "grouped"
|
|
|
39
42
|
|
|
40
43
|
[tool.ruff.lint]
|
|
41
44
|
select = [
|
|
42
|
-
"
|
|
45
|
+
"A",
|
|
46
|
+
"ANN",
|
|
47
|
+
"ARG",
|
|
48
|
+
"ASYNC",
|
|
49
|
+
"B",
|
|
50
|
+
"BLE",
|
|
51
|
+
"C4",
|
|
52
|
+
"COM",
|
|
53
|
+
"DTZ",
|
|
43
54
|
"E",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
55
|
+
"EM",
|
|
56
|
+
"EXE",
|
|
57
|
+
"F",
|
|
58
|
+
"FA",
|
|
59
|
+
"FBT",
|
|
60
|
+
"FURB",
|
|
46
61
|
"I",
|
|
62
|
+
"ICN",
|
|
63
|
+
"ISC",
|
|
47
64
|
"N",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
65
|
+
"PERF",
|
|
66
|
+
"PIE",
|
|
67
|
+
"PL",
|
|
68
|
+
"PT",
|
|
69
|
+
"PTH",
|
|
70
|
+
"Q",
|
|
71
|
+
"RSE",
|
|
72
|
+
"RUF",
|
|
50
73
|
"S",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"C4",
|
|
74
|
+
"SIM",
|
|
75
|
+
"SLOT",
|
|
76
|
+
"T10",
|
|
55
77
|
"T20",
|
|
56
|
-
"
|
|
57
|
-
"ARG",
|
|
78
|
+
"TC",
|
|
58
79
|
"TD",
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
"A002",
|
|
64
|
-
"A003",
|
|
65
|
-
"ANN401",
|
|
66
|
-
"C901",
|
|
67
|
-
"N8",
|
|
68
|
-
"B008",
|
|
69
|
-
"F405",
|
|
70
|
-
"F821",
|
|
71
|
-
"TD003",
|
|
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
|
|
|
91
|
-
[tool.
|
|
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]
|
|
92
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
|
|
93
120
|
|
|
94
121
|
[tool.uv.build-backend]
|
|
95
122
|
module-name = "python_intl"
|
|
@@ -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,15 +2,15 @@ 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
6
|
from typing import TYPE_CHECKING, overload
|
|
8
7
|
|
|
9
|
-
import icu
|
|
8
|
+
import icu
|
|
10
9
|
|
|
11
10
|
from .locale import Locale
|
|
12
11
|
|
|
13
12
|
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable, Iterable
|
|
14
14
|
from typing import NotRequired, TypedDict
|
|
15
15
|
|
|
16
16
|
from ._types import CaseFirstT, ComparisonResultT, LocaleMatcherT, SensitivityT
|
|
@@ -54,10 +54,10 @@ class CollatorOptions:
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
_COLLATOR_RESULT_TO_RESULT: dict[icu.UCollationResult, ComparisonResultT] = {
|
|
58
|
-
icu.UCollationResult.LESS: -1,
|
|
59
|
-
icu.UCollationResult.EQUAL: 0,
|
|
60
|
-
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,
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
|
|
@@ -82,11 +82,11 @@ class Collator:
|
|
|
82
82
|
self.options = CollatorOptions(**options)
|
|
83
83
|
|
|
84
84
|
@cached_property
|
|
85
|
-
def _icu_collator(self) -> icu.Collator:
|
|
86
|
-
collator = icu.Collator.createInstance(self.locale._icu_locale)
|
|
85
|
+
def _icu_collator(self) -> icu.Collator:
|
|
86
|
+
collator = icu.Collator.createInstance(self.locale._icu_locale)
|
|
87
87
|
|
|
88
88
|
if self.options.numeric:
|
|
89
|
-
collator.setAttribute(icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON)
|
|
89
|
+
collator.setAttribute(icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON)
|
|
90
90
|
|
|
91
91
|
if (
|
|
92
92
|
self.options.ignore_punctuation
|
|
@@ -95,24 +95,24 @@ class Collator:
|
|
|
95
95
|
and self.locale._icu_locale.getLanguage() == "th"
|
|
96
96
|
)
|
|
97
97
|
):
|
|
98
|
-
collator.setAttribute(icu.UCollAttribute.ALTERNATE_HANDLING, icu.UCollAttributeValue.SHIFTED)
|
|
98
|
+
collator.setAttribute(icu.UCollAttribute.ALTERNATE_HANDLING, icu.UCollAttributeValue.SHIFTED)
|
|
99
99
|
|
|
100
100
|
match self.options.case_first:
|
|
101
101
|
case "upper":
|
|
102
|
-
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.UPPER_FIRST)
|
|
102
|
+
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.UPPER_FIRST)
|
|
103
103
|
case "lower":
|
|
104
|
-
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.LOWER_FIRST)
|
|
104
|
+
collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.LOWER_FIRST)
|
|
105
105
|
|
|
106
|
-
collator.setAttribute(icu.UCollAttribute.NORMALIZATION_MODE, icu.UCollAttributeValue.ON)
|
|
106
|
+
collator.setAttribute(icu.UCollAttribute.NORMALIZATION_MODE, icu.UCollAttributeValue.ON)
|
|
107
107
|
match self.options.sensitivity:
|
|
108
108
|
case "base":
|
|
109
|
-
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.PRIMARY)
|
|
109
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.PRIMARY)
|
|
110
110
|
case "accent":
|
|
111
|
-
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.SECONDARY)
|
|
111
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.SECONDARY)
|
|
112
112
|
case "case":
|
|
113
|
-
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.TERTIARY)
|
|
113
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.TERTIARY)
|
|
114
114
|
case "variant":
|
|
115
|
-
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.QUATERNARY)
|
|
115
|
+
collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.QUATERNARY)
|
|
116
116
|
|
|
117
117
|
return collator
|
|
118
118
|
|
|
@@ -123,7 +123,7 @@ class Collator:
|
|
|
123
123
|
def sorted(self, items: Iterable[str], /, key: None = None) -> Iterable[str]: ...
|
|
124
124
|
@overload
|
|
125
125
|
def sorted[T](self, items: Iterable[T], /, key: Callable[[T], str]) -> Iterable[T]: ...
|
|
126
|
-
def sorted(self, items, /, key = None):
|
|
126
|
+
def sorted(self, items, /, key = None): # pyright: ignore[reportInconsistentOverload]
|
|
127
127
|
return sorted(
|
|
128
128
|
items,
|
|
129
129
|
key=functools.cmp_to_key(
|
|
@@ -1,15 +1,15 @@
|
|
|
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
|
|
9
8
|
|
|
10
9
|
from .locale import Locale
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
12
|
+
import datetime as dt
|
|
13
13
|
from collections.abc import Iterable
|
|
14
14
|
from typing import NotRequired, TypedDict
|
|
15
15
|
|
|
@@ -89,27 +89,27 @@ _PATTERN_SYMBOL_TO_TYPE: dict[str, DatetimePatternPartTypeT] = {
|
|
|
89
89
|
"x": "time_zone_name",
|
|
90
90
|
"X": "time_zone_name",
|
|
91
91
|
}
|
|
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",
|
|
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
99
|
# TODO(ddanier): Use this instead once PyICU has those values in the enum:
|
|
100
100
|
# https://gitlab.pyicu.org/main/pyicu/-/work_items/180
|
|
101
101
|
# icu.DateFormat.AM_PM_MIDNIGHT_NOON_FIELD: "day_period",
|
|
102
102
|
# icu.DateFormat.FLEXIBLE_DAY_PERIOD_FIELD: "day_period",
|
|
103
103
|
35: "day_period",
|
|
104
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",
|
|
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",
|
|
113
113
|
}
|
|
114
114
|
_PATTERN_QUOTE = "'"
|
|
115
115
|
|
|
@@ -172,7 +172,7 @@ class DateTimeFormatOptions:
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
|
|
175
|
-
def _options_to_possible_skeletons(options: DateTimeFormatOptions) -> Iterable[str]:
|
|
175
|
+
def _options_to_possible_skeletons(options: DateTimeFormatOptions) -> Iterable[str]: # noqa: PLR0912, PLR0915
|
|
176
176
|
skeleton_parts: list[str | tuple[str, ...]] = []
|
|
177
177
|
|
|
178
178
|
# Note: The parts should be ordered from big to small.
|
|
@@ -304,11 +304,12 @@ class _MatchedFormatPattern:
|
|
|
304
304
|
skeleton: str
|
|
305
305
|
pattern: str
|
|
306
306
|
|
|
307
|
+
@override
|
|
307
308
|
def __str__(self) -> str:
|
|
308
309
|
return self.pattern
|
|
309
310
|
|
|
310
311
|
|
|
311
|
-
class
|
|
312
|
+
class FormatPatternNotFoundError(Exception):
|
|
312
313
|
pass
|
|
313
314
|
|
|
314
315
|
|
|
@@ -342,12 +343,12 @@ class DateTimeIntervalPatternPart:
|
|
|
342
343
|
|
|
343
344
|
@cache
|
|
344
345
|
def _options_to_format_pattern(
|
|
345
|
-
locale: icu.Locale,
|
|
346
|
+
locale: icu.Locale,
|
|
346
347
|
options: DateTimeFormatOptions,
|
|
347
348
|
) -> _MatchedFormatPattern:
|
|
348
349
|
possible_skeletons = list(_options_to_possible_skeletons(options))
|
|
349
350
|
|
|
350
|
-
generator = icu.DateTimePatternGenerator.createInstance(locale)
|
|
351
|
+
generator = icu.DateTimePatternGenerator.createInstance(locale)
|
|
351
352
|
|
|
352
353
|
# Try a perfect match
|
|
353
354
|
for skeleton in possible_skeletons:
|
|
@@ -368,7 +369,8 @@ def _options_to_format_pattern(
|
|
|
368
369
|
pattern=pattern,
|
|
369
370
|
)
|
|
370
371
|
|
|
371
|
-
|
|
372
|
+
error = "Didn't find pattern for desired options"
|
|
373
|
+
raise FormatPatternNotFoundError(error)
|
|
372
374
|
|
|
373
375
|
|
|
374
376
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
@@ -381,7 +383,7 @@ class _PartSpan:
|
|
|
381
383
|
return cls(start=0, end=0)
|
|
382
384
|
|
|
383
385
|
@classmethod
|
|
384
|
-
def from_constrained_fieldposition(cls, position: icu.ConstrainedFieldPosition) -> _PartSpan:
|
|
386
|
+
def from_constrained_fieldposition(cls, position: icu.ConstrainedFieldPosition) -> _PartSpan:
|
|
385
387
|
return cls(start=position.getStart(), end=position.getLimit())
|
|
386
388
|
|
|
387
389
|
def __contains__(self, inner: _PartSpan) -> bool:
|
|
@@ -417,8 +419,8 @@ class DateTimeFormat:
|
|
|
417
419
|
return self._matched_pattern.pattern
|
|
418
420
|
|
|
419
421
|
@cached_property
|
|
420
|
-
def _icu_date_format(self) -> icu.SimpleDateFormat:
|
|
421
|
-
return icu.SimpleDateFormat(self._icu_pattern, self.locale._icu_locale)
|
|
422
|
+
def _icu_date_format(self) -> icu.SimpleDateFormat:
|
|
423
|
+
return icu.SimpleDateFormat(self._icu_pattern, self.locale._icu_locale)
|
|
422
424
|
|
|
423
425
|
def format(self, datetime_: dt.datetime, /) -> str:
|
|
424
426
|
return self._icu_date_format.format(datetime_)
|
|
@@ -438,7 +440,7 @@ class DateTimeFormat:
|
|
|
438
440
|
if char != prev_char and count > 0:
|
|
439
441
|
yield DateTimePatternPart(
|
|
440
442
|
type=_PATTERN_SYMBOL_TO_TYPE.get(prev_char, "unknown"),
|
|
441
|
-
value=icu.SimpleDateFormat(prev_char * count, self.locale._icu_locale).format(datetime_),
|
|
443
|
+
value=icu.SimpleDateFormat(prev_char * count, self.locale._icu_locale).format(datetime_),
|
|
442
444
|
_pattern=prev_char * count,
|
|
443
445
|
)
|
|
444
446
|
count = 0
|
|
@@ -466,7 +468,7 @@ class DateTimeFormat:
|
|
|
466
468
|
if count > 0:
|
|
467
469
|
yield DateTimePatternPart(
|
|
468
470
|
type=_PATTERN_SYMBOL_TO_TYPE.get(prev_char, "unknown"),
|
|
469
|
-
value=icu.SimpleDateFormat(prev_char * count, self.locale._icu_locale).format(datetime_),
|
|
471
|
+
value=icu.SimpleDateFormat(prev_char * count, self.locale._icu_locale).format(datetime_),
|
|
470
472
|
_pattern=prev_char * count,
|
|
471
473
|
)
|
|
472
474
|
assert not literal_chars # noqa: S101
|
|
@@ -477,16 +479,16 @@ class DateTimeFormat:
|
|
|
477
479
|
)
|
|
478
480
|
|
|
479
481
|
@cached_property
|
|
480
|
-
def _icu_dateinterval_format(self) -> icu.DateIntervalFormat:
|
|
482
|
+
def _icu_dateinterval_format(self) -> icu.DateIntervalFormat:
|
|
481
483
|
possible_skeletons = list(_options_to_possible_skeletons(self.options))
|
|
482
|
-
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self.locale._icu_locale)
|
|
484
|
+
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self.locale._icu_locale)
|
|
483
485
|
|
|
484
486
|
def format_range(
|
|
485
487
|
self,
|
|
486
488
|
start_datetime: dt.datetime,
|
|
487
489
|
end_datetime: dt.datetime,
|
|
488
490
|
) -> str:
|
|
489
|
-
icu_date_interval = icu.DateInterval(start_datetime, end_datetime)
|
|
491
|
+
icu_date_interval = icu.DateInterval(start_datetime, end_datetime)
|
|
490
492
|
return self._icu_dateinterval_format.format(icu_date_interval)
|
|
491
493
|
|
|
492
494
|
def format_range_to_parts(
|
|
@@ -494,14 +496,14 @@ class DateTimeFormat:
|
|
|
494
496
|
start_datetime: dt.datetime,
|
|
495
497
|
end_datetime: dt.datetime,
|
|
496
498
|
) -> Iterable[DateTimeIntervalPatternPart]:
|
|
497
|
-
icu_date_interval = icu.DateInterval(start_datetime, end_datetime)
|
|
499
|
+
icu_date_interval = icu.DateInterval(start_datetime, end_datetime)
|
|
498
500
|
formatted = self._icu_dateinterval_format.formatToValue(icu_date_interval)
|
|
499
501
|
|
|
500
502
|
# Find spans of both datetimes (used to determine which parts have which source)
|
|
501
503
|
span_start = _PartSpan.empty()
|
|
502
504
|
span_end = _PartSpan.empty()
|
|
503
505
|
for part in formatted:
|
|
504
|
-
if part.getCategory() == icu.UFieldCategory.DATE_INTERVAL_SPAN:
|
|
506
|
+
if part.getCategory() == icu.UFieldCategory.DATE_INTERVAL_SPAN:
|
|
505
507
|
match part.getField():
|
|
506
508
|
case 0:
|
|
507
509
|
span_start = _PartSpan.from_constrained_fieldposition(part)
|
|
@@ -528,7 +530,7 @@ class DateTimeFormat:
|
|
|
528
530
|
source=source_of(_PartSpan(start=last_end, end=span.start)),
|
|
529
531
|
)
|
|
530
532
|
|
|
531
|
-
if part.getCategory() == icu.UFieldCategory.DATE:
|
|
533
|
+
if part.getCategory() == icu.UFieldCategory.DATE:
|
|
532
534
|
yield DateTimeIntervalPatternPart(
|
|
533
535
|
type=_PATTERN_FIELD_TO_TYPE.get(part.getField(), "unknown"),
|
|
534
536
|
value=result_string[span.start:span.end],
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from functools import cached_property
|
|
4
4
|
|
|
5
|
-
import icu
|
|
5
|
+
import icu
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Locale:
|
|
@@ -12,5 +12,5 @@ class Locale:
|
|
|
12
12
|
self.tag = tag
|
|
13
13
|
|
|
14
14
|
@cached_property
|
|
15
|
-
def _icu_locale(self) -> icu.Locale:
|
|
16
|
-
return icu.Locale(self.tag)
|
|
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,63 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "python-intl"
|
|
3
|
-
version = "0.8.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
|
-
"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
|
-
output-format = "grouped"
|
|
40
|
-
|
|
41
|
-
[tool.ruff.lint]
|
|
42
|
-
select = ["F","E","W","C","I","N","UP","ANN","S","B","A","COM","C4","T20","PT","ARG","TD","RUF"]
|
|
43
|
-
ignore = ["A001","A002","A003","ANN401","C901","N8","B008","F405","F821","TD003"]
|
|
44
|
-
|
|
45
|
-
[tool.ruff.lint.per-file-ignores]
|
|
46
|
-
"__init__.py" = ["F401"]
|
|
47
|
-
"conftest.py" = ["S101","ANN","F401"]
|
|
48
|
-
"test_*.py" = ["S101","ANN","F401"]
|
|
49
|
-
|
|
50
|
-
[tool.ruff.lint.isort]
|
|
51
|
-
combine-as-imports = true
|
|
52
|
-
force-wrap-aliases = true
|
|
53
|
-
|
|
54
|
-
[tool.pyright]
|
|
55
|
-
pythonVersion = "3.12"
|
|
56
|
-
|
|
57
|
-
[build-system]
|
|
58
|
-
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
59
|
-
build-backend = "uv_build"
|
|
60
|
-
|
|
61
|
-
[tool.uv.build-backend]
|
|
62
|
-
module-name = "python_intl"
|
|
63
|
-
module-root = ""
|
|
@@ -1,34 +0,0 @@
|
|
|
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]
|
|
File without changes
|