Coverage for rendercv/data/models/computers.py: 99%
135 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-26 00:25 +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 import locale
18def format_phone_number(phone_number: str) -> str:
19 """Format a phone number to the format specified in the `locale` 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["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 get_date_input() -> Date:
46 """Return the date input.
48 Returns:
49 The date input.
50 """
51 from .rendercv_settings import DATE_INPUT
53 return DATE_INPUT
56def format_date(date: Date, date_template: Optional[str] = None) -> str:
57 """Formats a `Date` object to a string in the following format: "Jan 2021". The
58 month names are taken from the `locale` dictionary from the
59 `rendercv.data_models.models` module.
61 Example:
62 ```python
63 format_date(Date(2024, 5, 1))
64 ```
65 will return
67 `"May 2024"`
69 Args:
70 date: The date to format.
71 date_template: The template of the date string. If not provided, the default date
72 style from the `locale` dictionary will be used.
74 Returns:
75 The formatted date.
76 """
77 full_month_names = locale["full_names_of_months"]
78 short_month_names = locale["abbreviations_for_months"]
80 month = int(date.strftime("%m"))
81 year = date.strftime(format="%Y")
83 placeholders = {
84 "FULL_MONTH_NAME": full_month_names[month - 1],
85 "MONTH_ABBREVIATION": short_month_names[month - 1],
86 "MONTH_IN_TWO_DIGITS": f"{month:02d}",
87 "YEAR_IN_TWO_DIGITS": str(year[-2:]),
88 "MONTH": str(month),
89 "YEAR": str(year),
90 }
91 if date_template is None:
92 date_template = locale["date_template"] # type: ignore
94 assert isinstance(date_template, str)
96 for placeholder, value in placeholders.items():
97 date_template = date_template.replace(placeholder, value) # type: ignore
99 return date_template
102def replace_placeholders(value: str) -> str:
103 """Replaces the placeholders in a string with the corresponding values."""
104 name = curriculum_vitae.get("name", "None")
105 full_month_names = locale["full_names_of_months"]
106 short_month_names = locale["abbreviations_for_months"]
108 month = get_date_input().month
109 year = str(get_date_input().year)
111 placeholders = {
112 "NAME_IN_SNAKE_CASE": name.replace(" ", "_"),
113 "NAME_IN_LOWER_SNAKE_CASE": name.replace(" ", "_").lower(),
114 "NAME_IN_UPPER_SNAKE_CASE": name.replace(" ", "_").upper(),
115 "NAME_IN_KEBAB_CASE": name.replace(" ", "-"),
116 "NAME_IN_LOWER_KEBAB_CASE": name.replace(" ", "-").lower(),
117 "NAME_IN_UPPER_KEBAB_CASE": name.replace(" ", "-").upper(),
118 "FULL_MONTH_NAME": full_month_names[month - 1],
119 "MONTH_ABBREVIATION": short_month_names[month - 1],
120 "MONTH_IN_TWO_DIGITS": f"{month:02d}",
121 "YEAR_IN_TWO_DIGITS": str(year[-2:]),
122 "NAME": name,
123 "YEAR": str(year),
124 "MONTH": str(month),
125 }
127 for placeholder, placeholder_value in placeholders.items():
128 value = value.replace(placeholder, placeholder_value)
130 return value
133def convert_string_to_path(value: str) -> pathlib.Path:
134 """Converts a string to a `pathlib.Path` object by replacing the placeholders
135 with the corresponding values. If the path is not an absolute path, it is
136 converted to an absolute path by prepending the current working directory.
137 """
138 value = replace_placeholders(value)
140 return pathlib.Path(value).absolute()
143def compute_time_span_string(
144 start_date: Optional[str | int],
145 end_date: Optional[str | int],
146 date: Optional[str | int],
147) -> str:
148 """
149 Return a time span string based on the provided dates.
151 Example:
152 ```python
153 get_time_span_string("2020-01-01", "2020-05-01", None)
154 ```
156 returns
158 `"4 months"`
160 Args:
161 start_date: A start date in YYYY-MM-DD, YYYY-MM, or YYYY format.
162 end_date: An end date in YYYY-MM-DD, YYYY-MM, or YYYY format or "present".
163 date: A date in YYYY-MM-DD, YYYY-MM, or YYYY format or a custom string. If
164 provided, start_date and end_date will be ignored.
166 Returns:
167 The computed time span string.
168 """
169 date_is_provided = date is not None
170 start_date_is_provided = start_date is not None
171 end_date_is_provided = end_date is not None
173 if date_is_provided:
174 # If only the date is provided, the time span is irrelevant. So, return an
175 # empty string.
176 return ""
178 if not start_date_is_provided and not end_date_is_provided:
179 # If neither start_date nor end_date is provided, return an empty string.
180 return ""
182 if isinstance(start_date, int) or isinstance(end_date, int):
183 # Then it means one of the dates is year, so time span cannot be more
184 # specific than years.
185 start_year = get_date_object(start_date).year # type: ignore
186 end_year = get_date_object(end_date).year # type: ignore
188 time_span_in_years = end_year - start_year
190 if time_span_in_years < 2:
191 time_span_string = "1 year"
192 else:
193 time_span_string = f"{time_span_in_years} years"
195 return time_span_string
197 # Then it means both start_date and end_date are in YYYY-MM-DD or YYYY-MM
198 # format.
199 end_date = get_date_object(end_date) # type: ignore
200 start_date = get_date_object(start_date) # type: ignore
202 # Calculate the number of days between start_date and end_date:
203 timespan_in_days = (end_date - start_date).days # type: ignore
205 # Calculate the number of years and months between start_date and end_date:
206 how_many_years = timespan_in_days // 365
207 how_many_months = (timespan_in_days % 365) // 30 + 1
208 # Deal with overflow (prevent rounding to 1 year 12 months, etc.)
209 how_many_years += how_many_months // 12
210 how_many_months %= 12
212 # Format the number of years and months between start_date and end_date:
213 if how_many_years == 0:
214 how_many_years_string = None
215 elif how_many_years == 1:
216 how_many_years_string = f"1 {locale['year']}"
217 else:
218 how_many_years_string = f"{how_many_years} {locale['years']}"
220 # Format the number of months between start_date and end_date:
221 if how_many_months == 1 or (how_many_years_string is None and how_many_months == 0):
222 how_many_months_string = f"1 {locale['month']}"
223 elif how_many_months == 0:
224 how_many_months_string = None
225 else:
226 how_many_months_string = f"{how_many_months} {locale['months']}"
228 # Combine howManyYearsString and howManyMonthsString:
229 if how_many_years_string is None and how_many_months_string is not None:
230 time_span_string = how_many_months_string
231 elif how_many_months_string is None and how_many_years_string is not None:
232 time_span_string = how_many_years_string
233 elif how_many_years_string is not None and how_many_months_string is not None:
234 time_span_string = f"{how_many_years_string} {how_many_months_string}"
235 else:
236 message = "The time span is not valid!"
237 raise ValueError(message)
239 return time_span_string.strip()
242def compute_date_string(
243 start_date: Optional[str | int],
244 end_date: Optional[str | int],
245 date: Optional[str | int],
246 show_only_years: bool = False,
247) -> str:
248 """Return a date string based on the provided dates.
250 Example:
251 ```python
252 get_date_string("2020-01-01", "2021-01-01", None)
253 ```
254 returns
255 ```
256 "Jan 2020 to Jan 2021"
257 ```
259 Args:
260 start_date: A start date in YYYY-MM-DD, YYYY-MM, or YYYY format.
261 end_date: An end date in YYYY-MM-DD, YYYY-MM, or YYYY format or "present".
262 date: A date in YYYY-MM-DD, YYYY-MM, or YYYY format or a custom string. If
263 provided, start_date and end_date will be ignored.
264 show_only_years: If True, only the years will be shown in the date string.
266 Returns:
267 The computed date string.
268 """
269 date_is_provided = date is not None
270 start_date_is_provided = start_date is not None
271 end_date_is_provided = end_date is not None
273 if date_is_provided:
274 if isinstance(date, int):
275 # Then it means only the year is provided
276 date_string = str(date)
277 else:
278 try:
279 date_object = get_date_object(date)
280 if show_only_years:
281 date_string = str(date_object.year)
282 else:
283 date_string = format_date(date_object)
284 except ValueError:
285 # Then it is a custom date string (e.g., "My Custom Date")
286 date_string = str(date)
287 elif start_date_is_provided and end_date_is_provided:
288 if isinstance(start_date, int):
289 # Then it means only the year is provided
290 start_date = str(start_date)
291 else:
292 # Then it means start_date is either in YYYY-MM-DD or YYYY-MM format
293 date_object = get_date_object(start_date)
294 if show_only_years:
295 start_date = date_object.year
296 else:
297 start_date = format_date(date_object)
299 if end_date == "present":
300 end_date = locale["present"] # type: ignore
301 elif isinstance(end_date, int):
302 # Then it means only the year is provided
303 end_date = str(end_date)
304 else:
305 # Then it means end_date is either in YYYY-MM-DD or YYYY-MM format
306 date_object = get_date_object(end_date)
307 end_date = date_object.year if show_only_years else format_date(date_object)
309 date_string = f"{start_date} {locale['to']} {end_date}"
311 else:
312 # Neither date, start_date, nor end_date are provided, so return an empty
313 # string:
314 date_string = ""
316 return date_string
319def make_a_url_clean(url: str) -> str:
320 """Make a URL clean by removing the protocol, www, and trailing slashes.
322 Example:
323 ```python
324 make_a_url_clean("https://www.example.com/")
325 ```
326 returns
327 `"example.com"`
329 Args:
330 url: The URL to make clean.
332 Returns:
333 The clean URL.
334 """
335 url = url.replace("https://", "").replace("http://", "")
336 if url.endswith("/"):
337 url = url[:-1]
339 return url
342def get_date_object(date: str | int) -> Date:
343 """Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a
344 `datetime.date` object. This function is used throughout the validation process of
345 the data models.
347 Args:
348 date: The date string to parse.
350 Returns:
351 The parsed date.
352 """
353 if isinstance(date, int):
354 date_object = Date.fromisoformat(f"{date}-01-01")
355 elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
356 # Then it is in YYYY-MM-DD format
357 date_object = Date.fromisoformat(date)
358 elif re.fullmatch(r"\d{4}-\d{2}", date):
359 # Then it is in YYYY-MM format
360 date_object = Date.fromisoformat(f"{date}-01")
361 elif re.fullmatch(r"\d{4}", date):
362 # Then it is in YYYY format
363 date_object = Date.fromisoformat(f"{date}-01-01")
364 elif date == "present":
365 date_object = get_date_input()
366 else:
367 message = (
368 "This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or"
369 " YYYY format."
370 )
371 raise ValueError(message)
373 return date_object
376def dictionary_key_to_proper_section_title(key: str) -> str:
377 """Convert a dictionary key to a proper section title.
379 Example:
380 ```python
381 dictionary_key_to_proper_section_title("section_title")
382 ```
383 returns
384 `"Section Title"`
386 Args:
387 key: The key to convert to a proper section title.
389 Returns:
390 The proper section title.
391 """
392 title = key.replace("_", " ")
393 words = title.split(" ")
395 words_not_capitalized_in_a_title = [
396 "a",
397 "and",
398 "as",
399 "at",
400 "but",
401 "by",
402 "for",
403 "from",
404 "if",
405 "in",
406 "into",
407 "like",
408 "near",
409 "nor",
410 "of",
411 "off",
412 "on",
413 "onto",
414 "or",
415 "over",
416 "so",
417 "than",
418 "that",
419 "to",
420 "upon",
421 "when",
422 "with",
423 "yet",
424 ]
426 # loop through the words and if the word doesn't contain any uppercase letters,
427 # capitalize the first letter of the word. If the word contains uppercase letters,
428 # don't change the word.
429 return " ".join(
430 (
431 word.capitalize()
432 if (word.islower() and word not in words_not_capitalized_in_a_title)
433 else word
434 )
435 for word in words
436 )