Coverage for rendercv/data/models/computers.py: 100%
123 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-07 17:51 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-07 17:51 +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 (str): The phone number to format.
33 Returns:
34 str: The formatted phone number.
35 """
37 format = LOCALE_CATALOG["phone_number_format"].upper() # type: ignore
39 parsed_number = phonenumbers.parse(phone_number, None)
40 formatted_number = phonenumbers.format_number(
41 parsed_number, getattr(phonenumbers.PhoneNumberFormat, format)
42 )
43 return formatted_number
46def format_date(date: Date, date_style: Optional[str] = None) -> str:
47 """Formats a `Date` object to a string in the following format: "Jan 2021". The
48 month names are taken from the `locale_catalog` dictionary from the
49 `rendercv.data_models.models` module.
51 Example:
52 ```python
53 format_date(Date(2024, 5, 1))
54 ```
55 will return
57 `#!python "May 2024"`
59 Args:
60 date (Date): The date to format.
61 date_style (Optional[str]): The style of the date string. If not provided, the
62 default date style from the `locale_catalog` dictionary will be used.
64 Returns:
65 str: The formatted date.
66 """
67 full_month_names = LOCALE_CATALOG["full_names_of_months"]
68 short_month_names = LOCALE_CATALOG["abbreviations_for_months"]
70 month = int(date.strftime("%m"))
71 year = date.strftime(format="%Y")
73 placeholders = {
74 "FULL_MONTH_NAME": full_month_names[month - 1],
75 "MONTH_ABBREVIATION": short_month_names[month - 1],
76 "MONTH_IN_TWO_DIGITS": f"{month:02d}",
77 "YEAR_IN_TWO_DIGITS": str(year[-2:]),
78 "MONTH": str(month),
79 "YEAR": str(year),
80 }
81 if date_style is None:
82 date_style = LOCALE_CATALOG["date_style"] # type: ignore
84 for placeholder, value in placeholders.items():
85 date_style = date_style.replace(placeholder, value) # type: ignore
87 date_string = date_style
89 return date_string # type: ignore
92def convert_string_to_path(value: str) -> pathlib.Path:
93 """Converts a string to a `pathlib.Path` object by replacing the placeholders
94 with the corresponding values. If the path is not an absolute path, it is
95 converted to an absolute path by prepending the current working directory.
96 """
97 name = curriculum_vitae["name"] # Curriculum Vitae owner's name
98 full_month_names = LOCALE_CATALOG["full_names_of_months"]
99 short_month_names = LOCALE_CATALOG["abbreviations_for_months"]
101 month = Date.today().month
102 year = str(Date.today().year)
104 placeholders = {
105 "NAME_IN_SNAKE_CASE": name.replace(" ", "_"),
106 "NAME_IN_LOWER_SNAKE_CASE": name.replace(" ", "_").lower(),
107 "NAME_IN_UPPER_SNAKE_CASE": name.replace(" ", "_").upper(),
108 "NAME_IN_KEBAB_CASE": name.replace(" ", "-"),
109 "NAME_IN_LOWER_KEBAB_CASE": name.replace(" ", "-").lower(),
110 "NAME_IN_UPPER_KEBAB_CASE": name.replace(" ", "-").upper(),
111 "FULL_MONTH_NAME": full_month_names[month - 1],
112 "MONTH_ABBREVIATION": short_month_names[month - 1],
113 "MONTH_IN_TWO_DIGITS": f"{month:02d}",
114 "YEAR_IN_TWO_DIGITS": str(year[-2:]),
115 "NAME": name,
116 "YEAR": str(year),
117 "MONTH": str(month),
118 }
120 for placeholder, placeholder_value in placeholders.items():
121 value = value.replace(placeholder, placeholder_value)
123 return pathlib.Path(value).absolute()
126def compute_time_span_string(
127 start_date: Optional[str | int],
128 end_date: Optional[str | int],
129 date: Optional[str | int],
130) -> str:
131 """
132 Return a time span string based on the provided dates.
134 Example:
135 ```python
136 get_time_span_string("2020-01-01", "2020-05-01", None)
137 ```
139 returns
141 `#!python "4 months"`
143 Args:
144 start_date (Optional[str]): A start date in YYYY-MM-DD, YYYY-MM, or YYYY format.
145 end_date (Optional[str]): An end date in YYYY-MM-DD, YYYY-MM, or YYYY format or
146 "present".
147 date (Optional[str]): A date in YYYY-MM-DD, YYYY-MM, or YYYY format or a custom
148 string. If provided, start_date and end_date will be ignored.
150 Returns:
151 str: The computed time span string.
152 """
153 date_is_provided = date is not None
154 start_date_is_provided = start_date is not None
155 end_date_is_provided = end_date is not None
157 if date_is_provided:
158 # If only the date is provided, the time span is irrelevant. So, return an
159 # empty string.
160 return ""
162 elif not start_date_is_provided and not end_date_is_provided:
163 # If neither start_date nor end_date is provided, return an empty string.
164 return ""
166 elif isinstance(start_date, int) or isinstance(end_date, int):
167 # Then it means one of the dates is year, so time span cannot be more
168 # specific than years.
169 start_year = get_date_object(start_date).year # type: ignore
170 end_year = get_date_object(end_date).year # type: ignore
172 time_span_in_years = end_year - start_year
174 if time_span_in_years < 2:
175 time_span_string = "1 year"
176 else:
177 time_span_string = f"{time_span_in_years} years"
179 return time_span_string
181 else:
182 # Then it means both start_date and end_date are in YYYY-MM-DD or YYYY-MM
183 # format.
184 end_date = get_date_object(end_date) # type: ignore
185 start_date = get_date_object(start_date) # type: ignore
187 # Calculate the number of days between start_date and end_date:
188 timespan_in_days = (end_date - start_date).days # type: ignore
190 # Calculate the number of years between start_date and end_date:
191 how_many_years = timespan_in_days // 365
192 if how_many_years == 0:
193 how_many_years_string = None
194 elif how_many_years == 1:
195 how_many_years_string = f"1 {LOCALE_CATALOG['year']}"
196 else:
197 how_many_years_string = f"{how_many_years} {LOCALE_CATALOG['years']}"
199 # Calculate the number of months between start_date and end_date:
200 how_many_months = round((timespan_in_days % 365) / 30)
201 if how_many_months <= 1:
202 how_many_months_string = f"1 {LOCALE_CATALOG['month']}"
203 else:
204 how_many_months_string = f"{how_many_months} {LOCALE_CATALOG['months']}"
206 # Combine howManyYearsString and howManyMonthsString:
207 if how_many_years_string is None:
208 time_span_string = how_many_months_string
209 else:
210 time_span_string = f"{how_many_years_string} {how_many_months_string}"
212 return time_span_string
215def compute_date_string(
216 start_date: Optional[str | int],
217 end_date: Optional[str | int],
218 date: Optional[str | int],
219 show_only_years: bool = False,
220) -> str:
221 """Return a date string based on the provided dates.
223 Example:
224 ```python
225 get_date_string("2020-01-01", "2021-01-01", None)
226 ```
227 returns
228 ```
229 "Jan 2020 to Jan 2021"
230 ```
232 Args:
233 start_date (Optional[str]): A start date in YYYY-MM-DD, YYYY-MM, or YYYY
234 format.
235 end_date (Optional[str]): An end date in YYYY-MM-DD, YYYY-MM, or YYYY format
236 or "present".
237 date (Optional[str]): A date in YYYY-MM-DD, YYYY-MM, or YYYY format or
238 a custom string. If provided, start_date and end_date will be ignored.
239 show_only_years (bool): If True, only the years will be shown in the date
240 string.
242 Returns:
243 str: The computed date string.
244 """
245 date_is_provided = date is not None
246 start_date_is_provided = start_date is not None
247 end_date_is_provided = end_date is not None
249 if date_is_provided:
250 if isinstance(date, int):
251 # Then it means only the year is provided
252 date_string = str(date)
253 else:
254 try:
255 date_object = get_date_object(date)
256 if show_only_years:
257 date_string = str(date_object.year)
258 else:
259 date_string = format_date(date_object)
260 except ValueError:
261 # Then it is a custom date string (e.g., "My Custom Date")
262 date_string = str(date)
263 elif start_date_is_provided and end_date_is_provided:
264 if isinstance(start_date, int):
265 # Then it means only the year is provided
266 start_date = str(start_date)
267 else:
268 # Then it means start_date is either in YYYY-MM-DD or YYYY-MM format
269 date_object = get_date_object(start_date)
270 if show_only_years:
271 start_date = date_object.year
272 else:
273 start_date = format_date(date_object)
275 if end_date == "present":
276 end_date = LOCALE_CATALOG["present"] # type: ignore
277 elif isinstance(end_date, int):
278 # Then it means only the year is provided
279 end_date = str(end_date)
280 else:
281 # Then it means end_date is either in YYYY-MM-DD or YYYY-MM format
282 date_object = get_date_object(end_date)
283 if show_only_years:
284 end_date = date_object.year
285 else:
286 end_date = format_date(date_object)
288 date_string = f"{start_date} {LOCALE_CATALOG['to']} {end_date}"
290 else:
291 # Neither date, start_date, nor end_date are provided, so return an empty
292 # string:
293 date_string = ""
295 return date_string
298def make_a_url_clean(url: str) -> str:
299 """Make a URL clean by removing the protocol, www, and trailing slashes.
301 Example:
302 ```python
303 make_a_url_clean("https://www.example.com/")
304 ```
305 returns
306 `#!python "example.com"`
308 Args:
309 url (str): The URL to make clean.
311 Returns:
312 str: The clean URL.
313 """
314 url = url.replace("https://", "").replace("http://", "")
315 if url.endswith("/"):
316 url = url[:-1]
318 return url
321def get_date_object(date: str | int) -> Date:
322 """Parse a date string in YYYY-MM-DD, YYYY-MM, or YYYY format and return a
323 `datetime.date` object. This function is used throughout the validation process of
324 the data models.
326 Args:
327 date (str | int): The date string to parse.
329 Returns:
330 Date: The parsed date.
331 """
332 if isinstance(date, int):
333 date_object = Date.fromisoformat(f"{date}-01-01")
334 elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
335 # Then it is in YYYY-MM-DD format
336 date_object = Date.fromisoformat(date)
337 elif re.fullmatch(r"\d{4}-\d{2}", date):
338 # Then it is in YYYY-MM format
339 date_object = Date.fromisoformat(f"{date}-01")
340 elif re.fullmatch(r"\d{4}", date):
341 # Then it is in YYYY format
342 date_object = Date.fromisoformat(f"{date}-01-01")
343 elif date == "present":
344 date_object = Date.today()
345 else:
346 raise ValueError(
347 "This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or"
348 " YYYY format."
349 )
351 return date_object
354def dictionary_key_to_proper_section_title(key: str) -> str:
355 """Convert a dictionary key to a proper section title.
357 Example:
358 ```python
359 dictionary_key_to_proper_section_title("section_title")
360 ```
361 returns
362 `#!python "Section Title"`
364 Args:
365 key (str): The key to convert to a proper section title.
367 Returns:
368 str: The proper section title.
369 """
370 title = key.replace("_", " ")
371 words = title.split(" ")
373 words_not_capitalized_in_a_title = [
374 "a",
375 "and",
376 "as",
377 "at",
378 "but",
379 "by",
380 "for",
381 "from",
382 "if",
383 "in",
384 "into",
385 "like",
386 "near",
387 "nor",
388 "of",
389 "off",
390 "on",
391 "onto",
392 "or",
393 "over",
394 "so",
395 "than",
396 "that",
397 "to",
398 "upon",
399 "when",
400 "with",
401 "yet",
402 ]
404 # loop through the words and if the word doesn't contain any uppercase letters,
405 # capitalize the first letter of the word. If the word contains uppercase letters,
406 # don't change the word.
407 proper_title = " ".join(
408 (
409 word.capitalize()
410 if (word.islower() and word not in words_not_capitalized_in_a_title)
411 else word
412 )
413 for word in words
414 )
416 return proper_title