1import datetime
2import decimal
3import functools
4import re
5import unicodedata
6from importlib import import_module
7
8from django.conf import settings
9from django.utils import dateformat, numberformat
10from django.utils.functional import lazy
11from django.utils.translation import check_for_language, get_language, to_locale
12
13# format_cache is a mapping from (format_type, lang) to the format string.
14# By using the cache, it is possible to avoid running get_format_modules
15# repeatedly.
16_format_cache = {}
17_format_modules_cache = {}
18
19ISO_INPUT_FORMATS = {
20 "DATE_INPUT_FORMATS": ["%Y-%m-%d"],
21 "TIME_INPUT_FORMATS": ["%H:%M:%S", "%H:%M:%S.%f", "%H:%M"],
22 "DATETIME_INPUT_FORMATS": [
23 "%Y-%m-%d %H:%M:%S",
24 "%Y-%m-%d %H:%M:%S.%f",
25 "%Y-%m-%d %H:%M",
26 "%Y-%m-%d",
27 ],
28}
29
30
31FORMAT_SETTINGS = frozenset(
32 [
33 "DECIMAL_SEPARATOR",
34 "THOUSAND_SEPARATOR",
35 "NUMBER_GROUPING",
36 "FIRST_DAY_OF_WEEK",
37 "MONTH_DAY_FORMAT",
38 "TIME_FORMAT",
39 "DATE_FORMAT",
40 "DATETIME_FORMAT",
41 "SHORT_DATE_FORMAT",
42 "SHORT_DATETIME_FORMAT",
43 "YEAR_MONTH_FORMAT",
44 "DATE_INPUT_FORMATS",
45 "TIME_INPUT_FORMATS",
46 "DATETIME_INPUT_FORMATS",
47 ]
48)
49
50
51def reset_format_cache():
52 """Clear any cached formats.
53
54 This method is provided primarily for testing purposes,
55 so that the effects of cached formats can be removed.
56 """
57 global _format_cache, _format_modules_cache
58 _format_cache = {}
59 _format_modules_cache = {}
60
61
62def iter_format_modules(lang, format_module_path=None):
63 """Find format modules."""
64 if not check_for_language(lang):
65 return
66
67 if format_module_path is None:
68 format_module_path = settings.FORMAT_MODULE_PATH
69
70 format_locations = []
71 if format_module_path:
72 if isinstance(format_module_path, str):
73 format_module_path = [format_module_path]
74 for path in format_module_path:
75 format_locations.append(path + ".%s")
76 format_locations.append("django.conf.locale.%s")
77 locale = to_locale(lang)
78 locales = [locale]
79 if "_" in locale:
80 locales.append(locale.split("_")[0])
81 for location in format_locations:
82 for loc in locales:
83 try:
84 yield import_module("%s.formats" % (location % loc))
85 except ImportError:
86 pass
87
88
89def get_format_modules(lang=None):
90 """Return a list of the format modules found."""
91 if lang is None:
92 lang = get_language()
93 if lang not in _format_modules_cache:
94 _format_modules_cache[lang] = list(
95 iter_format_modules(lang, settings.FORMAT_MODULE_PATH)
96 )
97 return _format_modules_cache[lang]
98
99
100def get_format(format_type, lang=None, use_l10n=None):
101 """
102 For a specific format type, return the format for the current
103 language (locale). Default to the format in the settings.
104 format_type is the name of the format, e.g. 'DATE_FORMAT'.
105
106 If use_l10n is provided and is not None, it forces the value to
107 be localized (or not), otherwise it's always localized.
108 """
109 if use_l10n is None:
110 use_l10n = True
111 if use_l10n and lang is None:
112 lang = get_language()
113 format_type = str(format_type) # format_type may be lazy.
114 cache_key = (format_type, lang)
115 try:
116 return _format_cache[cache_key]
117 except KeyError:
118 pass
119
120 # The requested format_type has not been cached yet. Try to find it in any
121 # of the format_modules for the given lang if l10n is enabled. If it's not
122 # there or if l10n is disabled, fall back to the project settings.
123 val = None
124 if use_l10n:
125 for module in get_format_modules(lang):
126 val = getattr(module, format_type, None)
127 if val is not None:
128 break
129 if val is None:
130 if format_type not in FORMAT_SETTINGS:
131 return format_type
132 val = getattr(settings, format_type)
133 elif format_type in ISO_INPUT_FORMATS:
134 # If a list of input formats from one of the format_modules was
135 # retrieved, make sure the ISO_INPUT_FORMATS are in this list.
136 val = list(val)
137 for iso_input in ISO_INPUT_FORMATS.get(format_type, ()):
138 if iso_input not in val:
139 val.append(iso_input)
140 _format_cache[cache_key] = val
141 return val
142
143
144get_format_lazy = lazy(get_format, str, list, tuple)
145
146
147def date_format(value, format=None, use_l10n=None):
148 """
149 Format a datetime.date or datetime.datetime object using a
150 localizable format.
151
152 If use_l10n is provided and is not None, that will force the value to
153 be localized (or not), otherwise it's always localized.
154 """
155 return dateformat.format(
156 value, get_format(format or "DATE_FORMAT", use_l10n=use_l10n)
157 )
158
159
160def time_format(value, format=None, use_l10n=None):
161 """
162 Format a datetime.time object using a localizable format.
163
164 If use_l10n is provided and is not None, it forces the value to
165 be localized (or not), otherwise it's always localized.
166 """
167 return dateformat.time_format(
168 value, get_format(format or "TIME_FORMAT", use_l10n=use_l10n)
169 )
170
171
172def number_format(value, decimal_pos=None, use_l10n=None, force_grouping=False):
173 """
174 Format a numeric value using localization settings.
175
176 If use_l10n is provided and is not None, it forces the value to
177 be localized (or not), otherwise it's always localized.
178 """
179 if use_l10n is None:
180 use_l10n = True
181 lang = get_language() if use_l10n else None
182 return numberformat.format(
183 value,
184 get_format("DECIMAL_SEPARATOR", lang, use_l10n=use_l10n),
185 decimal_pos,
186 get_format("NUMBER_GROUPING", lang, use_l10n=use_l10n),
187 get_format("THOUSAND_SEPARATOR", lang, use_l10n=use_l10n),
188 force_grouping=force_grouping,
189 use_l10n=use_l10n,
190 )
191
192
193def localize(value, use_l10n=None):
194 """
195 Check if value is a localizable type (date, number...) and return it
196 formatted as a string using current locale format.
197
198 If use_l10n is provided and is not None, it forces the value to
199 be localized (or not), otherwise it's always localized.
200 """
201 if isinstance(value, str): # Handle strings first for performance reasons.
202 return value
203 elif isinstance(value, bool): # Make sure booleans don't get treated as numbers
204 return str(value)
205 elif isinstance(value, (decimal.Decimal, float, int)):
206 if use_l10n is False:
207 return str(value)
208 return number_format(value, use_l10n=use_l10n)
209 elif isinstance(value, datetime.datetime):
210 return date_format(value, "DATETIME_FORMAT", use_l10n=use_l10n)
211 elif isinstance(value, datetime.date):
212 return date_format(value, use_l10n=use_l10n)
213 elif isinstance(value, datetime.time):
214 return time_format(value, use_l10n=use_l10n)
215 return value
216
217
218def localize_input(value, default=None):
219 """
220 Check if an input value is a localizable type and return it
221 formatted with the appropriate formatting string of the current locale.
222 """
223 if isinstance(value, str): # Handle strings first for performance reasons.
224 return value
225 elif isinstance(value, bool): # Don't treat booleans as numbers.
226 return str(value)
227 elif isinstance(value, (decimal.Decimal, float, int)):
228 return number_format(value)
229 elif isinstance(value, datetime.datetime):
230 format = default or get_format("DATETIME_INPUT_FORMATS")[0]
231 format = sanitize_strftime_format(format)
232 return value.strftime(format)
233 elif isinstance(value, datetime.date):
234 format = default or get_format("DATE_INPUT_FORMATS")[0]
235 format = sanitize_strftime_format(format)
236 return value.strftime(format)
237 elif isinstance(value, datetime.time):
238 format = default or get_format("TIME_INPUT_FORMATS")[0]
239 return value.strftime(format)
240 return value
241
242
243@functools.lru_cache
244def sanitize_strftime_format(fmt):
245 """
246 Ensure that certain specifiers are correctly padded with leading zeros.
247
248 For years < 1000 specifiers %C, %F, %G, and %Y don't work as expected for
249 strftime provided by glibc on Linux as they don't pad the year or century
250 with leading zeros. Support for specifying the padding explicitly is
251 available, however, which can be used to fix this issue.
252
253 FreeBSD, macOS, and Windows do not support explicitly specifying the
254 padding, but return four digit years (with leading zeros) as expected.
255
256 This function checks whether the %Y produces a correctly padded string and,
257 if not, makes the following substitutions:
258
259 - %C → %02C
260 - %F → %010F
261 - %G → %04G
262 - %Y → %04Y
263
264 See https://bugs.python.org/issue13305 for more details.
265 """
266 if datetime.date(1, 1, 1).strftime("%Y") == "0001":
267 return fmt
268 mapping = {"C": 2, "F": 10, "G": 4, "Y": 4}
269 return re.sub(
270 r"((?:^|[^%])(?:%%)*)%([CFGY])",
271 lambda m: r"%s%%0%s%s" % (m[1], mapping[m[2]], m[2]),
272 fmt,
273 )
274
275
276def sanitize_separators(value):
277 """
278 Sanitize a value according to the current decimal and
279 thousand separator setting. Used with form field input.
280 """
281 if isinstance(value, str):
282 parts = []
283 decimal_separator = get_format("DECIMAL_SEPARATOR")
284 if decimal_separator in value:
285 value, decimals = value.split(decimal_separator, 1)
286 parts.append(decimals)
287 if settings.USE_THOUSAND_SEPARATOR:
288 thousand_sep = get_format("THOUSAND_SEPARATOR")
289 if (
290 thousand_sep == "."
291 and value.count(".") == 1
292 and len(value.split(".")[-1]) != 3
293 ):
294 # Special case where we suspect a dot meant decimal separator
295 # (see #22171).
296 pass
297 else:
298 for replacement in {
299 thousand_sep,
300 unicodedata.normalize("NFKD", thousand_sep),
301 }:
302 value = value.replace(replacement, "")
303 parts.append(value)
304 value = ".".join(reversed(parts))
305 return value