Coverage for rendercv/data/models/computers.py: 98%
132 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-12-25 23:06 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-12-25 23:06 +0000
1"""
2The `rendercv.data.models.computers` module contains functions that compute some
3properties based on the input data. For example, it includes functions that calculate
4the time span between two dates, the date string, the URL of a social network, etc.
5"""
7import pathlib
8import re
9from datetime import date as Date
10from typing import Optional
12import phonenumbers
14from .curriculum_vitae import curriculum_vitae
15from .locale_catalog import LOCALE_CATALOG
18def format_phone_number(phone_number: str) -> str:
19 """Format a phone number to the format specified in the `locale_catalog` dictionary.
21 Example:
22 ```python
23 format_phone_number("+17034800500")
24 ```
25 returns
26 ```python
27 "(703) 480-0500"
28 ```
30 Args:
31 phone_number: The phone number to format.
33 Returns:
34 The formatted phone number.
35 """
37 format = LOCALE_CATALOG["phone_number_format"].upper() # type: ignore
39 parsed_number = phonenumbers.parse(phone_number, None)
40 return phonenumbers.format_number(
41 parsed_number, getattr(phonenumbers.PhoneNumberFormat, format)
42 )
45def format_date(date: Date, date_style: Optional[str] = None) -> str:
46 """Formats a `Date` object to a string in the following format: "Jan 2021". The
47 month names are taken from the `locale_catalog` dictionary from the
48 `rendercv.data_models.models` module.
50 Example:
51 ```python
52 format_date(Date(2024, 5, 1))
53 ```
54 will return
56 `"May 2024"`
58 Args:
59 date: The date to format.
60 date_style: The style of the date string. If not provided, the default date
61 style from the `locale_catalog` dictionary will be used.
63 Returns:
64 The formatted date.
65 """
66 full_month_names = LOCALE_CATALOG["full_names_of_months"]
67 short_month_names = LOCALE_CATALOG["abbreviations_for_months"]
69 month = int(date.strftime("%m"))
70 year = date.strftime(format="%Y")
72 placeholders = {
73 "FULL_MONTH_NAME": full_month_names[month - 1],
74 "MONTH_ABBREVIATION": short_month_names[month - 1],
75 "MONTH_IN_TWO_DIGITS": f"{month:02d}",
76 "YEAR_IN_TWO_DIGITS": str(year[-2:]),
77 "MONTH": str(month),
78 "YEAR": str(year),
79 }
80 if date_style is None:
81 date_style = LOCALE_CATALOG["date_style"] # type: ignore
83 assert isinstance(date_style, str)
85 for placeholder, value in placeholders.items():
86 date_style = date_style.replace(placeholder, value) # type: ignore
88 return date_style
91def replace_placeholders(value: str) -> str:
92 """Replaces the placeholders in a string with the corresponding values."""
93 name = curriculum_vitae.get("name", "None")
94 full_month_names = LOCALE_CATALOG["full_names_of_months"]
95 short_month_names = LOCALE_CATALOG["abbreviations_for_months"]
97 month = Date.today().month
98 year = str(Date.today().year)
100 placeholders = {
101 "NAME_IN_SNAKE_CASE": name.replace(" ", "_"),
102 "NAME_IN_LOWER_SNAKE_CASE": name.replace(" ", "_").lower(),
103 "NAME_IN_UPPER_SNAKE_CASE": name.replace(" ", "_").upper(),
104 "NAME_IN_KEBAB_CASE": name.replace(" ", "-"),
105 "NAME_IN_LOWER_KEBAB_CASE": name.replace(" ", "-").lower(),
106 "NAME_IN_UPPER_KEBAB_CASE": name.replace(" ", "-").upper(),
107 "FULL_MONTH_NAME": full_month_names[month - 1],
108 "MONTH_ABBREVIATION": short_month_names[month - 1],
109 "MONTH_IN_TWO_DIGITS": f"{month:02d}",
110 "YEAR_IN_TWO_DIGITS": str(year[-2:]),
111 "NAME": name,
112 "YEAR": str(year),
113 "MONTH": str(month),
114 }
116 for placeholder, placeholder_value in placeholders.items():
117 value = value.replace(placeholder, placeholder_value)
119 return value
122def convert_string_to_path(value: str) -> pathlib.Path:
123 """Converts a string to a `pathlib.Path` object by replacing the placeholders
124 with the corresponding values. If the path is not an absolute path, it is
125 converted to an absolute path by prepending the current working directory.
126 """
127 value = replace_placeholders(value)
129 return pathlib.Path(value).absolute()
132def compute_time_span_string(
133 start_date: Optional[str | int],
134 end_date: Optional[str | int],
135 date: Optional[str | int],
136) -> str:
137 """
138 Return a time span string based on the provided dates.
140 Example:
141 ```python
142 get_time_span_string("2020-01-01", "2020-05-01", None)
143 ```
145 returns
147 `"4 months"`
149 Args:
150 start_date: A start date in YYYY-MM-DD, YYYY-MM, or YYYY format.
151 end_date: An end date in YYYY-MM-DD, YYYY-MM, or YYYY format or "present".
152 date: A date in YYYY-MM-DD, YYYY-MM, or YYYY format or a custom string. If
153 provided, start_date and end_date will be ignored.
155 Returns:
156 The computed time span string.
157 """
158 date_is_provided = date is not None
159 start_date_is_provided = start_date is not None
160 end_date_is_provided = end_date is not None
162 if date_is_provided:
163 # If only the date is provided, the time span is irrelevant. So, return an
164 # empty string.
165 return ""
167 if not start_date_is_provided and not end_date_is_provided:
168 # If neither start_date nor end_date is provided, return an empty string.
169 return ""
171 if isinstance(start_date, int) or isinstance(end_date, int):
172 # Then it means one of the dates is year, so time span cannot be more
173 # specific than years.
174 start_year = get_date_object(start_date).year # type: ignore
175 end_year = get_date_object(end_date).year # type: ignore
177 time_span_in_years = end_year - start_year
179 if time_span_in_years < 2:
180 time_span_string = "1 year"
181 else:
182 time_span_string = f"{time_span_in_years} years"
184 return time_span_string
186 # Then it means both start_date and end_date are in YYYY-MM-DD or YYYY-MM
187 # format.
188 end_date = get_date_object(end_date) # type: ignore
189 start_date = get_date_object(start_date) # type: ignore
191 # Calculate the number of days between start_date and end_date:
192 timespan_in_days = (end_date - start_date).days # type: ignore
194 # Calculate the number of years and months between start_date and end_date:
195 how_many_years = timespan_in_days // 365
196 how_many_months = (timespan_in_days % 365) // 30 + 1
197 # Deal with overflow (prevent rounding to 1 year 12 months, etc.)
198 how_many_years += how_many_months // 12
199 how_many_months %= 12
201 # Format the number of years and months between start_date and end_date:
202 if how_many_years == 0:
203 how_many_years_string = None
204 elif how_many_years == 1:
205 how_many_years_string = f"1 {LOCALE_CATALOG['year']}"
206 else:
207 how_many_years_string = f"{how_many_years} {LOCALE_CATALOG['years']}"
209 # Format the number of months between start_date and end_date:
210 if how_many_months == 1 or (how_many_years_string is None and how_many_months == 0):
211 how_many_months_string = f"1 {LOCALE_CATALOG['month']}"
212 elif how_many_months == 0:
213 how_many_months_string = None
214 else:
215 how_many_months_string = f"{how_many_months} {LOCALE_CATALOG['months']}"
217 # Combine howManyYearsString and howManyMonthsString:
218 if how_many_years_string is None and how_many_months_string is not None:
219 time_span_string = how_many_months_string
220 elif how_many_months_string is None and how_many_years_string is not None:
221 time_span_string = how_many_years_string
222 elif how_many_years_string is not None and how_many_months_string is not None:
223 time_span_string = f"{how_many_years_string} {how_many_months_string}"
224 else:
225 message = "The time span is not valid!"
226 raise ValueError(message)
228 return time_span_string.strip()
231def compute_date_string(
232 start_date: Optional[str | int],
233 end_date: Optional[str | int],
234 date: Optional[str | int],
235 show_only_years: bool = False,
236) -> str:
237 """Return a date string based on the provided dates.
239 Example:
240 ```python
241 get_date_string("2020-01-01", "2021-01-01", None)
242 ```
243 returns
244 ```
245 "Jan 2020 to Jan 2021"
246 ```
248 Args:
249 start_date: A start date in YYYY-MM-DD, YYYY-MM, or YYYY format.
250 end_date: An end date in YYYY-MM-DD, YYYY-MM, or YYYY format or "present".
251 date: A date in YYYY-MM-DD, YYYY-MM, or YYYY format or a custom string. If
252 provided, start_date and end_date will be ignored.
253 show_only_years: If True, only the years will be shown in the date string.
255 Returns:
256 The computed date string.
257 """
258 date_is_provided = date is not None
259 start_date_is_provided = start_date is not None
260 end_date_is_provided = end_date is not None
262 if date_is_provided:
263 if isinstance(date, int):
264 # Then it means only the year is provided
265 date_string = str(date)
266 else:
267 try:
268 date_object = get_date_object(date)
269 if show_only_years:
270 date_string = str(date_object.year)
271 else:
272 date_string = format_date(date_object)
273 except ValueError:
274 # Then it is a custom date string (e.g., "My Custom Date")
275 date_string = str(date)
276 elif start_date_is_provided and end_date_is_provided:
277 if isinstance(start_date, int):
278 # Then it means only the year is provided
279 start_date = str(start_date)
280 else:
281 # Then it means start_date is either in YYYY-MM-DD or YYYY-MM format
282 date_object = get_date_object(start_date)
283 if show_only_years:
284 start_date = date_object.year
285 else:
286 start_date = format_date(date_object)
288 if end_date == "present":
289 end_date = LOCALE_CATALOG["present"] # type: ignore
290 elif isinstance(end_date, int):
291 # Then it means only the year is provided
292 end_date = str(end_date)
293 else:
294 # Then it means end_date is either in YYYY-MM-DD or YYYY-MM format
295 date_object = get_date_object(end_date)
296 end_date = date_object.year if show_only_years else format_date(date_object)
298 date_string = f"{start_date} {LOCALE_CATALOG['to']} {end_date}"
300 else:
301 # Neither date, start_date, nor end_date are provided, so return an empty
302 # string:
303 date_string = ""
305 return date_string
308def make_a_url_clean(url: str) -> str:
309 """Make a URL clean by removing the protocol, www, and trailing slashes.
311 Example:
312 ```python
313 make_a_url_clean("https://www.example.com/")
314 ```
315 returns
316 `"example.com"`
318 Args:
319 url: The URL to make clean.
321 Returns:
322 The clean URL.
323 """
324 url = url.replace("https://", "").replace("http://", "")
325 if url.endswith("/"):
326 url = url[:-1]
328 return url
331def get_date_object(date: str | int) -> Date:
332 """Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a
333 `datetime.date` object. This function is used throughout the validation process of
334 the data models.
336 Args:
337 date: The date string to parse.
339 Returns:
340 The parsed date.
341 """
342 if isinstance(date, int):
343 date_object = Date.fromisoformat(f"{date}-01-01")
344 elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
345 # Then it is in YYYY-MM-DD format
346 date_object = Date.fromisoformat(date)
347 elif re.fullmatch(r"\d{4}-\d{2}", date):
348 # Then it is in YYYY-MM format
349 date_object = Date.fromisoformat(f"{date}-01")
350 elif re.fullmatch(r"\d{4}", date):
351 # Then it is in YYYY format
352 date_object = Date.fromisoformat(f"{date}-01-01")
353 elif date == "present":
354 date_object = Date.today()
355 else:
356 message = (
357 "This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or"
358 " YYYY format."
359 )
360 raise ValueError(message)
362 return date_object
365def dictionary_key_to_proper_section_title(key: str) -> str:
366 """Convert a dictionary key to a proper section title.
368 Example:
369 ```python
370 dictionary_key_to_proper_section_title("section_title")
371 ```
372 returns
373 `"Section Title"`
375 Args:
376 key: The key to convert to a proper section title.
378 Returns:
379 The proper section title.
380 """
381 title = key.replace("_", " ")
382 words = title.split(" ")
384 words_not_capitalized_in_a_title = [
385 "a",
386 "and",
387 "as",
388 "at",
389 "but",
390 "by",
391 "for",
392 "from",
393 "if",
394 "in",
395 "into",
396 "like",
397 "near",
398 "nor",
399 "of",
400 "off",
401 "on",
402 "onto",
403 "or",
404 "over",
405 "so",
406 "than",
407 "that",
408 "to",
409 "upon",
410 "when",
411 "with",
412 "yet",
413 ]
415 # loop through the words and if the word doesn't contain any uppercase letters,
416 # capitalize the first letter of the word. If the word contains uppercase letters,
417 # don't change the word.
418 return " ".join(
419 (
420 word.capitalize()
421 if (word.islower() and word not in words_not_capitalized_in_a_title)
422 else word
423 )
424 for word in words
425 )