python-intl 0.3.1__tar.gz → 0.5.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.3.1 → python_intl-0.5.0}/PKG-INFO +24 -12
- {python_intl-0.3.1 → python_intl-0.5.0}/README.md +23 -11
- {python_intl-0.3.1 → python_intl-0.5.0}/pyproject.toml +1 -1
- {python_intl-0.3.1 → python_intl-0.5.0}/pyproject.toml.orig +1 -1
- {python_intl-0.3.1 → python_intl-0.5.0}/python_intl/__init__.py +1 -0
- {python_intl-0.3.1 → python_intl-0.5.0}/python_intl/datetimeformat.py +119 -0
- {python_intl-0.3.1 → python_intl-0.5.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.5.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>
|
|
@@ -21,6 +21,11 @@ the "same" code works on JavaScript and Python, with similar results.
|
|
|
21
21
|
**Status:** This is very much work in progress. Currently only `Intl.DateTimeFormat`
|
|
22
22
|
exists and this also is not complete yet.
|
|
23
23
|
|
|
24
|
+
**Note:** There are a lot of tests running the Python implementation against the
|
|
25
|
+
JavaScript one and comparing the results. In general things should be pretty
|
|
26
|
+
stable there. Still there are some cases with known differences. Also note
|
|
27
|
+
that results depend on the ICU version you have installed on your machine.
|
|
28
|
+
|
|
24
29
|
## General notes about the Python adaption
|
|
25
30
|
|
|
26
31
|
All names will be using the Python style rules. This means instead of
|
|
@@ -55,24 +60,31 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
55
60
|
#### Example usage
|
|
56
61
|
|
|
57
62
|
```python
|
|
58
|
-
import datetime
|
|
63
|
+
import datetime as dt
|
|
59
64
|
import python_intl as Intl
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
# Format a datetime
|
|
67
|
+
datetime = dt.datetime(2026, 8, 15)
|
|
62
68
|
formatter = Intl.DateTimeFormat("de-DE", {"year": "numeric", "month": "2-digit", "day": "2-digit"})
|
|
63
|
-
formatter.format(
|
|
69
|
+
formatter.format(datetime)
|
|
70
|
+
# Result = "15.08.2026"
|
|
71
|
+
|
|
72
|
+
# Format a datetime range
|
|
73
|
+
datetime_till = dt.datetime(2026, 9, 7)
|
|
74
|
+
formatter.format_range(datetime, datetime_till)
|
|
75
|
+
# Result = "15.08. – 07.09.2026"
|
|
64
76
|
```
|
|
65
77
|
|
|
66
78
|
#### Compatibility
|
|
67
79
|
|
|
68
|
-
| Method | Status | Python name
|
|
69
|
-
| ----------------------------------- | :----: |
|
|
70
|
-
| `DateTimeFormat.format` | ✅ |
|
|
71
|
-
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts`
|
|
72
|
-
| `DateTimeFormat.supportedLocalesOf` | ❌ |
|
|
73
|
-
| `DateTimeFormat.formatRange` |
|
|
74
|
-
| `DateTimeFormat.formatRangeToParts` |
|
|
75
|
-
| `DateTimeFormat.resolvedOptions` | ❌ |
|
|
80
|
+
| Method | Status | Python name |
|
|
81
|
+
| ----------------------------------- | :----: | -------------------------------------- |
|
|
82
|
+
| `DateTimeFormat.format` | ✅ | |
|
|
83
|
+
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts` |
|
|
84
|
+
| `DateTimeFormat.supportedLocalesOf` | ❌ | |
|
|
85
|
+
| `DateTimeFormat.formatRange` | ✅ | `DateTimeFormat.format_range` |
|
|
86
|
+
| `DateTimeFormat.formatRangeToParts` | ✅ | `DateTimeFormat.format_range_to_parts` |
|
|
87
|
+
| `DateTimeFormat.resolvedOptions` | ❌ | |
|
|
76
88
|
|
|
77
89
|
## Installation
|
|
78
90
|
|
|
@@ -8,6 +8,11 @@ the "same" code works on JavaScript and Python, with similar results.
|
|
|
8
8
|
**Status:** This is very much work in progress. Currently only `Intl.DateTimeFormat`
|
|
9
9
|
exists and this also is not complete yet.
|
|
10
10
|
|
|
11
|
+
**Note:** There are a lot of tests running the Python implementation against the
|
|
12
|
+
JavaScript one and comparing the results. In general things should be pretty
|
|
13
|
+
stable there. Still there are some cases with known differences. Also note
|
|
14
|
+
that results depend on the ICU version you have installed on your machine.
|
|
15
|
+
|
|
11
16
|
## General notes about the Python adaption
|
|
12
17
|
|
|
13
18
|
All names will be using the Python style rules. This means instead of
|
|
@@ -42,24 +47,31 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
42
47
|
#### Example usage
|
|
43
48
|
|
|
44
49
|
```python
|
|
45
|
-
import datetime
|
|
50
|
+
import datetime as dt
|
|
46
51
|
import python_intl as Intl
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
# Format a datetime
|
|
54
|
+
datetime = dt.datetime(2026, 8, 15)
|
|
49
55
|
formatter = Intl.DateTimeFormat("de-DE", {"year": "numeric", "month": "2-digit", "day": "2-digit"})
|
|
50
|
-
formatter.format(
|
|
56
|
+
formatter.format(datetime)
|
|
57
|
+
# Result = "15.08.2026"
|
|
58
|
+
|
|
59
|
+
# Format a datetime range
|
|
60
|
+
datetime_till = dt.datetime(2026, 9, 7)
|
|
61
|
+
formatter.format_range(datetime, datetime_till)
|
|
62
|
+
# Result = "15.08. – 07.09.2026"
|
|
51
63
|
```
|
|
52
64
|
|
|
53
65
|
#### Compatibility
|
|
54
66
|
|
|
55
|
-
| Method | Status | Python name
|
|
56
|
-
| ----------------------------------- | :----: |
|
|
57
|
-
| `DateTimeFormat.format` | ✅ |
|
|
58
|
-
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts`
|
|
59
|
-
| `DateTimeFormat.supportedLocalesOf` | ❌ |
|
|
60
|
-
| `DateTimeFormat.formatRange` |
|
|
61
|
-
| `DateTimeFormat.formatRangeToParts` |
|
|
62
|
-
| `DateTimeFormat.resolvedOptions` | ❌ |
|
|
67
|
+
| Method | Status | Python name |
|
|
68
|
+
| ----------------------------------- | :----: | -------------------------------------- |
|
|
69
|
+
| `DateTimeFormat.format` | ✅ | |
|
|
70
|
+
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts` |
|
|
71
|
+
| `DateTimeFormat.supportedLocalesOf` | ❌ | |
|
|
72
|
+
| `DateTimeFormat.formatRange` | ✅ | `DateTimeFormat.format_range` |
|
|
73
|
+
| `DateTimeFormat.formatRangeToParts` | ✅ | `DateTimeFormat.format_range_to_parts` |
|
|
74
|
+
| `DateTimeFormat.resolvedOptions` | ❌ | |
|
|
63
75
|
|
|
64
76
|
## Installation
|
|
65
77
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .datetimeformat import (
|
|
2
2
|
DateTimeFormat as DateTimeFormat,
|
|
3
3
|
DateTimeFormatOptions as DateTimeFormatOptions,
|
|
4
|
+
DateTimeIntervalPatternPart as DateTimeIntervalPatternPart,
|
|
4
5
|
DateTimePatternPart as DateTimePatternPart,
|
|
5
6
|
FormatPatternNotFoundException as FormatPatternNotFoundException,
|
|
6
7
|
)
|
|
@@ -107,6 +107,22 @@ _PATTERN_SYMBOL_TO_TYPE: dict[str, PatternPartTypeT] = {
|
|
|
107
107
|
"x": "time_zone_name",
|
|
108
108
|
"X": "time_zone_name",
|
|
109
109
|
}
|
|
110
|
+
_PATTERN_FIELD_TO_TYPE: dict[icu.UDateTimePatternField, PatternPartTypeT] = { # ty: ignore[unresolved-attribute]
|
|
111
|
+
icu.DateFormat.ERA_FIELD: "era", # ty: ignore[unresolved-attribute]
|
|
112
|
+
icu.DateFormat.YEAR_FIELD: "year", # ty: ignore[unresolved-attribute]
|
|
113
|
+
icu.DateFormat.MONTH_FIELD: "month", # ty: ignore[unresolved-attribute]
|
|
114
|
+
icu.DateFormat.DAY_OF_WEEK_FIELD: "weekday", # ty: ignore[unresolved-attribute]
|
|
115
|
+
icu.DateFormat.DATE_FIELD: "day", # ty: ignore[unresolved-attribute]
|
|
116
|
+
icu.DateFormat.AM_PM_FIELD: "day_period", # ty: ignore[unresolved-attribute]
|
|
117
|
+
icu.DateFormat.HOUR0_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
118
|
+
icu.DateFormat.HOUR1_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
119
|
+
icu.DateFormat.HOUR_OF_DAY0_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
120
|
+
icu.DateFormat.HOUR_OF_DAY1_FIELD: "hour", # ty: ignore[unresolved-attribute]
|
|
121
|
+
icu.DateFormat.MINUTE_FIELD: "minute", # ty: ignore[unresolved-attribute]
|
|
122
|
+
icu.DateFormat.SECOND_FIELD: "second", # ty: ignore[unresolved-attribute]
|
|
123
|
+
icu.DateFormat.MILLISECOND_FIELD: "fraction_second_digits", # ty: ignore[unresolved-attribute]
|
|
124
|
+
icu.DateFormat.TIMEZONE_FIELD: "time_zone_name", # ty: ignore[unresolved-attribute]
|
|
125
|
+
}
|
|
110
126
|
_PATTERN_QUOTE = "'"
|
|
111
127
|
|
|
112
128
|
_COMPONENT_TO_JSON_MAP: dict[str, str] = {
|
|
@@ -314,6 +330,23 @@ class DateTimePatternPart:
|
|
|
314
330
|
}
|
|
315
331
|
|
|
316
332
|
|
|
333
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
334
|
+
class DateTimeIntervalPatternPart:
|
|
335
|
+
type: PatternPartTypeT
|
|
336
|
+
value: str
|
|
337
|
+
source: Literal["start_range", "end_range", "shared"]
|
|
338
|
+
|
|
339
|
+
def to_json(self) -> dict[str, str]:
|
|
340
|
+
return {
|
|
341
|
+
"type": _COMPONENT_TO_JSON_MAP.get(self.type, self.type),
|
|
342
|
+
"value": self.value,
|
|
343
|
+
"source": {
|
|
344
|
+
"start_range": "startRange",
|
|
345
|
+
"end_range": "endRange",
|
|
346
|
+
}.get(self.source, self.source),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
317
350
|
@cache
|
|
318
351
|
def _options_to_format_pattern(
|
|
319
352
|
locale: icu.Locale, # ty: ignore[unresolved-attribute]
|
|
@@ -345,6 +378,23 @@ def _options_to_format_pattern(
|
|
|
345
378
|
raise FormatPatternNotFoundException("Didn't find pattern for desired options")
|
|
346
379
|
|
|
347
380
|
|
|
381
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
382
|
+
class _PartSpan:
|
|
383
|
+
start: int
|
|
384
|
+
end: int
|
|
385
|
+
|
|
386
|
+
@classmethod
|
|
387
|
+
def empty(cls) -> _PartSpan:
|
|
388
|
+
return cls(start=0, end=0)
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
def from_constrained_fieldposition(cls, position: icu.ConstrainedFieldPosition) -> _PartSpan: # ty: ignore[unresolved-attribute]
|
|
392
|
+
return cls(start=position.getStart(), end=position.getLimit())
|
|
393
|
+
|
|
394
|
+
def __contains__(self, inner: _PartSpan) -> bool:
|
|
395
|
+
return inner.start >= self.start and inner.end <= self.end
|
|
396
|
+
|
|
397
|
+
|
|
348
398
|
class DateTimeFormat:
|
|
349
399
|
locale: str
|
|
350
400
|
options: DateTimeFormatOptions
|
|
@@ -431,3 +481,72 @@ class DateTimeFormat:
|
|
|
431
481
|
type="literal",
|
|
432
482
|
value="".join(literal_chars),
|
|
433
483
|
)
|
|
484
|
+
|
|
485
|
+
@cached_property
|
|
486
|
+
def _icu_dateinterval_format(self) -> icu.DateIntervalFormat: # ty: ignore[unresolved-attribute]
|
|
487
|
+
possible_skeletons = list(_options_to_possible_skeletons(self.options))
|
|
488
|
+
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self._icu_locale) # ty: ignore[unresolved-attribute]
|
|
489
|
+
|
|
490
|
+
def format_range(
|
|
491
|
+
self,
|
|
492
|
+
start_datetime: dt.datetime,
|
|
493
|
+
end_datetime: dt.datetime,
|
|
494
|
+
) -> str:
|
|
495
|
+
icu_date_interval = icu.DateInterval(start_datetime, end_datetime) # ty: ignore[unresolved-attribute]
|
|
496
|
+
return self._icu_dateinterval_format.format(icu_date_interval)
|
|
497
|
+
|
|
498
|
+
def format_range_to_parts(
|
|
499
|
+
self,
|
|
500
|
+
start_datetime: dt.datetime,
|
|
501
|
+
end_datetime: dt.datetime,
|
|
502
|
+
) -> Iterable[DateTimeIntervalPatternPart]:
|
|
503
|
+
icu_date_interval = icu.DateInterval(start_datetime, end_datetime) # ty: ignore[unresolved-attribute]
|
|
504
|
+
formatted = self._icu_dateinterval_format.formatToValue(icu_date_interval)
|
|
505
|
+
|
|
506
|
+
# Find spans of both datetimes (used to determine which parts have which source)
|
|
507
|
+
span_start = _PartSpan.empty()
|
|
508
|
+
span_end = _PartSpan.empty()
|
|
509
|
+
for part in formatted:
|
|
510
|
+
if part.getCategory() == icu.UFieldCategory.DATE_INTERVAL_SPAN: # ty: ignore[unresolved-attribute]
|
|
511
|
+
match part.getField():
|
|
512
|
+
case 0:
|
|
513
|
+
span_start = _PartSpan.from_constrained_fieldposition(part)
|
|
514
|
+
case 1:
|
|
515
|
+
span_end = _PartSpan.from_constrained_fieldposition(part)
|
|
516
|
+
|
|
517
|
+
def source_of(span: _PartSpan) -> Literal["start_range", "end_range", "shared"]:
|
|
518
|
+
if span in span_start:
|
|
519
|
+
return "start_range"
|
|
520
|
+
elif span in span_end:
|
|
521
|
+
return "end_range"
|
|
522
|
+
else:
|
|
523
|
+
return "shared"
|
|
524
|
+
|
|
525
|
+
# Break result string into parts
|
|
526
|
+
result_string = str(formatted)
|
|
527
|
+
last_end = 0
|
|
528
|
+
for part in formatted:
|
|
529
|
+
span = _PartSpan.from_constrained_fieldposition(part)
|
|
530
|
+
if span.start > last_end:
|
|
531
|
+
yield DateTimeIntervalPatternPart(
|
|
532
|
+
type="literal",
|
|
533
|
+
value=result_string[last_end:span.start],
|
|
534
|
+
source=source_of(_PartSpan(start=last_end, end=span.start)),
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
if part.getCategory() == icu.UFieldCategory.DATE: # ty: ignore[unresolved-attribute]
|
|
538
|
+
yield DateTimeIntervalPatternPart(
|
|
539
|
+
type=_PATTERN_FIELD_TO_TYPE.get(part.getField(), "unknown"),
|
|
540
|
+
value=result_string[span.start:span.end],
|
|
541
|
+
source=source_of(span),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
last_end = span.end
|
|
545
|
+
|
|
546
|
+
# Ensure we didn't miss anything at the end
|
|
547
|
+
if last_end < len(result_string):
|
|
548
|
+
yield DateTimeIntervalPatternPart(
|
|
549
|
+
type="literal",
|
|
550
|
+
value=result_string[last_end:],
|
|
551
|
+
source=source_of(_PartSpan(start=last_end, end=len(result_string))),
|
|
552
|
+
)
|
|
File without changes
|