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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-intl
3
- Version: 0.4.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) # Will output the German format: "15.08.2026"
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) # Will output the German format: "15.08. – 07.09.2026"
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) # Will output the German format: "15.08.2026"
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) # Will output the German format: "15.08. – 07.09.2026"
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,6 @@
1
1
  [project]
2
2
  name = "python-intl"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Python implementation of the Intl JavaScript API"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-intl"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Python implementation of the Intl JavaScript API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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(self, start_datetime: dt.datetime, end_datetime: dt.datetime) -> str:
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