python-intl 0.4.0__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.4.0 → python_intl-0.5.0}/PKG-INFO +16 -11
- {python_intl-0.4.0 → python_intl-0.5.0}/README.md +15 -10
- {python_intl-0.4.0 → python_intl-0.5.0}/pyproject.toml +1 -1
- {python_intl-0.4.0 → python_intl-0.5.0}/pyproject.toml.orig +1 -1
- {python_intl-0.4.0 → python_intl-0.5.0}/python_intl/__init__.py +1 -0
- {python_intl-0.4.0 → python_intl-0.5.0}/python_intl/datetimeformat.py +111 -1
- {python_intl-0.4.0 → 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>
|
|
@@ -63,23 +63,28 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
63
63
|
import datetime as dt
|
|
64
64
|
import python_intl as Intl
|
|
65
65
|
|
|
66
|
+
# Format a datetime
|
|
66
67
|
datetime = dt.datetime(2026, 8, 15)
|
|
67
68
|
formatter = Intl.DateTimeFormat("de-DE", {"year": "numeric", "month": "2-digit", "day": "2-digit"})
|
|
68
|
-
formatter.format(datetime)
|
|
69
|
+
formatter.format(datetime)
|
|
70
|
+
# Result = "15.08.2026"
|
|
71
|
+
|
|
72
|
+
# Format a datetime range
|
|
69
73
|
datetime_till = dt.datetime(2026, 9, 7)
|
|
70
|
-
formatter.format_range(datetime, datetime_till)
|
|
74
|
+
formatter.format_range(datetime, datetime_till)
|
|
75
|
+
# Result = "15.08. – 07.09.2026"
|
|
71
76
|
```
|
|
72
77
|
|
|
73
78
|
#### Compatibility
|
|
74
79
|
|
|
75
|
-
| Method | Status | Python name
|
|
76
|
-
| ----------------------------------- | :----: |
|
|
77
|
-
| `DateTimeFormat.format` | ✅ |
|
|
78
|
-
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts`
|
|
79
|
-
| `DateTimeFormat.supportedLocalesOf` | ❌ |
|
|
80
|
-
| `DateTimeFormat.formatRange` | ✅ | `DateTimeFormat.format_range`
|
|
81
|
-
| `DateTimeFormat.formatRangeToParts` |
|
|
82
|
-
| `DateTimeFormat.resolvedOptions` | ❌ |
|
|
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` | ❌ | |
|
|
83
88
|
|
|
84
89
|
## Installation
|
|
85
90
|
|
|
@@ -50,23 +50,28 @@ Intl.DateTimeFormatOptions(time_zone_name="short_offset").to_json()
|
|
|
50
50
|
import datetime as dt
|
|
51
51
|
import python_intl as Intl
|
|
52
52
|
|
|
53
|
+
# Format a datetime
|
|
53
54
|
datetime = dt.datetime(2026, 8, 15)
|
|
54
55
|
formatter = Intl.DateTimeFormat("de-DE", {"year": "numeric", "month": "2-digit", "day": "2-digit"})
|
|
55
|
-
formatter.format(datetime)
|
|
56
|
+
formatter.format(datetime)
|
|
57
|
+
# Result = "15.08.2026"
|
|
58
|
+
|
|
59
|
+
# Format a datetime range
|
|
56
60
|
datetime_till = dt.datetime(2026, 9, 7)
|
|
57
|
-
formatter.format_range(datetime, datetime_till)
|
|
61
|
+
formatter.format_range(datetime, datetime_till)
|
|
62
|
+
# Result = "15.08. – 07.09.2026"
|
|
58
63
|
```
|
|
59
64
|
|
|
60
65
|
#### Compatibility
|
|
61
66
|
|
|
62
|
-
| Method | Status | Python name
|
|
63
|
-
| ----------------------------------- | :----: |
|
|
64
|
-
| `DateTimeFormat.format` | ✅ |
|
|
65
|
-
| `DateTimeFormat.formatToParts` | ✅ | `DateTimeFormat.format_to_parts`
|
|
66
|
-
| `DateTimeFormat.supportedLocalesOf` | ❌ |
|
|
67
|
-
| `DateTimeFormat.formatRange` | ✅ | `DateTimeFormat.format_range`
|
|
68
|
-
| `DateTimeFormat.formatRangeToParts` |
|
|
69
|
-
| `DateTimeFormat.resolvedOptions` | ❌ |
|
|
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` | ❌ | |
|
|
70
75
|
|
|
71
76
|
## Installation
|
|
72
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
|
|
@@ -437,6 +487,66 @@ class DateTimeFormat:
|
|
|
437
487
|
possible_skeletons = list(_options_to_possible_skeletons(self.options))
|
|
438
488
|
return icu.DateIntervalFormat.createInstance(possible_skeletons[0], self._icu_locale) # ty: ignore[unresolved-attribute]
|
|
439
489
|
|
|
440
|
-
def format_range(
|
|
490
|
+
def format_range(
|
|
491
|
+
self,
|
|
492
|
+
start_datetime: dt.datetime,
|
|
493
|
+
end_datetime: dt.datetime,
|
|
494
|
+
) -> str:
|
|
441
495
|
icu_date_interval = icu.DateInterval(start_datetime, end_datetime) # ty: ignore[unresolved-attribute]
|
|
442
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
|