1from __future__ import annotations
2
3import typing as t
4
5from pendulum.locales.locale import Locale
6
7
8if t.TYPE_CHECKING:
9 from pendulum import Duration
10
11DAYS_THRESHOLD_FOR_HALF_WEEK = 3
12DAYS_THRESHOLD_FOR_HALF_MONTH = 15
13MONTHS_THRESHOLD_FOR_HALF_YEAR = 6
14
15HOURS_IN_NEARLY_A_DAY = 22
16DAYS_IN_NEARLY_A_MONTH = 27
17MONTHS_IN_NEARLY_A_YEAR = 11
18
19DAYS_OF_WEEK = 7
20SECONDS_OF_MINUTE = 60
21FEW_SECONDS_MAX = 10
22
23KEY_FUTURE = ".future"
24KEY_PAST = ".past"
25KEY_AFTER = ".after"
26KEY_BEFORE = ".before"
27
28
29class DifferenceFormatter:
30 """
31 Handles formatting differences in text.
32 """
33
34 def __init__(self, locale: str = "en") -> None:
35 self._locale = Locale.load(locale)
36
37 def format(
38 self,
39 diff: Duration,
40 is_now: bool = True,
41 absolute: bool = False,
42 locale: str | Locale | None = None,
43 ) -> str:
44 """
45 Formats a difference.
46
47 :param diff: The difference to format
48 :param is_now: Whether the difference includes now
49 :param absolute: Whether it's an absolute difference or not
50 :param locale: The locale to use
51 """
52 locale = self._locale if locale is None else Locale.load(locale)
53
54 if diff.years > 0:
55 unit = "year"
56 count = diff.years
57
58 if diff.months > MONTHS_THRESHOLD_FOR_HALF_YEAR:
59 count += 1
60 elif (diff.months == MONTHS_IN_NEARLY_A_YEAR) and (
61 (diff.weeks * DAYS_OF_WEEK + diff.remaining_days)
62 > DAYS_THRESHOLD_FOR_HALF_MONTH
63 ):
64 unit = "year"
65 count = 1
66 elif diff.months > 0:
67 unit = "month"
68 count = diff.months
69
70 if (
71 diff.weeks * DAYS_OF_WEEK + diff.remaining_days
72 ) >= DAYS_IN_NEARLY_A_MONTH:
73 count += 1
74 elif diff.weeks > 0:
75 unit = "week"
76 count = diff.weeks
77
78 if diff.remaining_days > DAYS_THRESHOLD_FOR_HALF_WEEK:
79 count += 1
80 elif diff.remaining_days > 0:
81 unit = "day"
82 count = diff.remaining_days
83
84 if diff.hours >= HOURS_IN_NEARLY_A_DAY:
85 count += 1
86 elif diff.hours > 0:
87 unit = "hour"
88 count = diff.hours
89 elif diff.minutes > 0:
90 unit = "minute"
91 count = diff.minutes
92 elif FEW_SECONDS_MAX < diff.remaining_seconds < SECONDS_OF_MINUTE:
93 unit = "second"
94 count = diff.remaining_seconds
95 else:
96 # We check if the "a few seconds" unit exists
97 time = locale.get("custom.units.few_second")
98 if time is not None:
99 if absolute:
100 return t.cast("str", time)
101
102 key = "custom"
103 is_future = diff.invert
104 if is_now:
105 if is_future:
106 key += ".from_now"
107 else:
108 key += ".ago"
109 else:
110 if is_future:
111 key += KEY_AFTER
112 else:
113 key += KEY_BEFORE
114
115 return t.cast("str", locale.get(key).format(time))
116 else:
117 unit = "second"
118 count = diff.remaining_seconds
119 if count == 0:
120 count = 1
121 if absolute:
122 key = f"translations.units.{unit}"
123 else:
124 is_future = diff.invert
125 if is_now:
126 # Relative to now, so we can use
127 # the CLDR data
128 key = f"translations.relative.{unit}"
129
130 if is_future:
131 key += KEY_FUTURE
132 else:
133 key += KEY_PAST
134 else:
135 # Absolute comparison
136 # So we have to use the custom locale data
137
138 # Checking for special pluralization rules
139 key = "custom.units_relative"
140 if is_future:
141 key += f".{unit}{KEY_FUTURE}"
142 else:
143 key += f".{unit}{KEY_PAST}"
144
145 trans = locale.get(key)
146 if not trans:
147 # No special rule
148 key = f"translations.units.{unit}.{locale.plural(count)}"
149 time = locale.get(key).format(count)
150 else:
151 time = trans[locale.plural(count)].format(count)
152
153 key = "custom"
154 if is_future:
155 key += KEY_AFTER
156 else:
157 key += KEY_BEFORE
158
159 return t.cast("str", locale.get(key).format(time))
160
161 key += f".{locale.plural(count)}"
162
163 return t.cast("str", locale.get(key).format(count))