refspy.utils
Provide general utility functions for the Refspy package.
1""" 2Provide general utility functions for the Refspy package. 3""" 4 5import re 6from typing import Any 7 8from refspy.constants import SPACE, NON_BREAKING_SPACE 9from refspy.types.number import Number 10 11 12def parse_number(number_str: str) -> Number: 13 """Remove non-digits and return an integer IF it falls between 1 and 999 14 inclusive. 15 16 Args: 17 number_str: A string expected to contain an integer, among other 18 characters. 19 20 Raises: 21 ValueError: if no digits in 1..999 range. 22 23 Return: 24 None if no digits appear in `number_str` or not in range. 25 """ 26 digits = re.sub(r"\D", "", number_str) 27 if digits != "": 28 number = int(digits) 29 if 1 <= number <= 999: 30 return number 31 raise ValueError(f"Invalid number string for parse_number: '{number_str}'") 32 33 34def url_escape(name: str) -> str: 35 """Turn a library or book name into a URL escaped string 36 37 Simple URL encode for significant characters: 38 39 * '1 Cor 3:4–5 -> '1%20Cor%203%3A4-5' 40 """ 41 return ( 42 name.replace(SPACE, "%20") 43 .replace(NON_BREAKING_SPACE, "%20") 44 .replace(":", "%3A") 45 .replace("–", "-") 46 ) 47 48 49def url_param(name: str) -> str: 50 """Turn a library or book name into a URL-friendly string. 51 52 Lowercase and no spaces: '1 Cor 3:4-5' -> '1+cor+3.4-5' 53 """ 54 return name.lower().replace(" ", "+").replace(":", ".").replace("–", "-") 55 56 57def strip_book_number(name: str) -> str: 58 """Remove any digit and space from the start of a string. 59 60 e.g. '2 Tim' becomes 'Tim'. 61 """ 62 return re.sub(r"^\d ", "", name) 63 64 65def get_unnumbered_book_aliases(book_aliases: dict) -> set: 66 unique = set() 67 for alias in book_aliases.keys(): 68 unique.add(strip_book_number(alias)) 69 return unique 70 71 72def strip_space_after_book_number(name: str) -> str: 73 """Remove space between any leading digit and all subsequent text. 74 75 e.g. '2 Tim' becomes '2Tim'. 76 """ 77 return re.sub(r"^(\d) (.*)$", r"\1\2", name) 78 79 80def add_space_after_book_number( 81 name: str, unnumbered_book_aliases: set, number_prefixes: dict[str, list[str]] 82) -> str: 83 """Remove space between any leading digit and all subsequent text. 84 85 - '2Tim' becomes '2 Tim'. 86 - 'SecondTim' or '2ndTim' or 'IITim' becomes '2 Tim' (use number_prefixes). 87 88 Note: Must search in reverse order, so 'II' is checked before 'I'. 89 """ 90 if " " in name: # Assume already done 91 return name 92 for number, prefixes in sorted(number_prefixes.items(), reverse=True): 93 for prefix in prefixes: 94 head = name[: len(prefix)] 95 tail = name[len(prefix) :] 96 if head == prefix and tail in unnumbered_book_aliases: 97 return number + " " + tail 98 return re.sub(r"^(\d)([A-Z])([a-z].*)$", r"\1 \2\3", name) 99 100 101def trim_trailing_period(text: str) -> str: 102 """Remove any tailing '.' character.""" 103 return text.rstrip(".") 104 105 106def normalize_spacing(text: str) -> str: 107 """Replace multiple spaces with single spaces. 108 109 Args: 110 text: A string expected to have irregular spacing. 111 """ 112 return re.sub("\\s+", " ", text) 113 114 115def pluralize(number: int, singular: str, plural: str = "") -> str: 116 """ 117 Super simple pluralization. 118 """ 119 if number == 1: 120 return "%d %s" % (number, singular) 121 else: 122 if plural != "": 123 return "%d %s" % (number, plural) 124 else: 125 return "%d %ss" % (number, singular) 126 127 128def sequential_replace_tuples(text: str, tuples: list[tuple[str, str]]): 129 """Replace values, searching from the end of each previous replacement. 130 131 This is for replacing matches, and ensures they are replaced in the 132 same order they are matched. 133 134 This function takes find-replace strings as a list of tuples. See 135 `refspy.utils.sequential_replace`. 136 137 Args: 138 text: A string in which matches should be replaces 139 tuples: A list of find-replace tuples, in order of appearance. 140 """ 141 new_text = "" 142 cursor = 0 143 for find, replace in tuples: 144 if find and replace: 145 next_match = text.find(find, cursor) 146 if next_match != -1: 147 new_text += text[cursor:next_match] 148 new_text += replace 149 cursor = next_match + len(find) 150 new_text += text[cursor:] 151 return new_text 152 153 154def sequential_replace(text: str, find: list[str], replace: list[str]) -> str: 155 """Replace values, searching from the end of each previous replacement. 156 157 This function takes find-replace strings as two separate lists. Unmatched 158 entries will be ignored by zip(). See 159 `refspy.utils.sequential_replace_tuples`. 160 161 Args: 162 text: A string in which matches should be replaces 163 find: A list of values to be found, in order of appearance. 164 replace: A list of replacement values. 165 """ 166 return sequential_replace_tuples(text, list(zip(find, replace))) 167 168 169def string_together(*args: Any) -> str: 170 """Convert objects to strings and concatenate. 171 172 Args: 173 *args: A list of string-convertable values. 174 """ 175 return "".join([str(arg) for arg in args])
13def parse_number(number_str: str) -> Number: 14 """Remove non-digits and return an integer IF it falls between 1 and 999 15 inclusive. 16 17 Args: 18 number_str: A string expected to contain an integer, among other 19 characters. 20 21 Raises: 22 ValueError: if no digits in 1..999 range. 23 24 Return: 25 None if no digits appear in `number_str` or not in range. 26 """ 27 digits = re.sub(r"\D", "", number_str) 28 if digits != "": 29 number = int(digits) 30 if 1 <= number <= 999: 31 return number 32 raise ValueError(f"Invalid number string for parse_number: '{number_str}'")
Remove non-digits and return an integer IF it falls between 1 and 999 inclusive.
Arguments:
- number_str: A string expected to contain an integer, among other characters.
Raises:
- ValueError: if no digits in 1..999 range.
Return:
None if no digits appear in
number_stror not in range.
35def url_escape(name: str) -> str: 36 """Turn a library or book name into a URL escaped string 37 38 Simple URL encode for significant characters: 39 40 * '1 Cor 3:4–5 -> '1%20Cor%203%3A4-5' 41 """ 42 return ( 43 name.replace(SPACE, "%20") 44 .replace(NON_BREAKING_SPACE, "%20") 45 .replace(":", "%3A") 46 .replace("–", "-") 47 )
Turn a library or book name into a URL escaped string
Simple URL encode for significant characters:
- '1 Cor 3:4–5 -> '1%20Cor%203%3A4-5'
50def url_param(name: str) -> str: 51 """Turn a library or book name into a URL-friendly string. 52 53 Lowercase and no spaces: '1 Cor 3:4-5' -> '1+cor+3.4-5' 54 """ 55 return name.lower().replace(" ", "+").replace(":", ".").replace("–", "-")
Turn a library or book name into a URL-friendly string.
Lowercase and no spaces: '1 Cor 3:4-5' -> '1+cor+3.4-5'
58def strip_book_number(name: str) -> str: 59 """Remove any digit and space from the start of a string. 60 61 e.g. '2 Tim' becomes 'Tim'. 62 """ 63 return re.sub(r"^\d ", "", name)
Remove any digit and space from the start of a string.
e.g. '2 Tim' becomes 'Tim'.
73def strip_space_after_book_number(name: str) -> str: 74 """Remove space between any leading digit and all subsequent text. 75 76 e.g. '2 Tim' becomes '2Tim'. 77 """ 78 return re.sub(r"^(\d) (.*)$", r"\1\2", name)
Remove space between any leading digit and all subsequent text.
e.g. '2 Tim' becomes '2Tim'.
81def add_space_after_book_number( 82 name: str, unnumbered_book_aliases: set, number_prefixes: dict[str, list[str]] 83) -> str: 84 """Remove space between any leading digit and all subsequent text. 85 86 - '2Tim' becomes '2 Tim'. 87 - 'SecondTim' or '2ndTim' or 'IITim' becomes '2 Tim' (use number_prefixes). 88 89 Note: Must search in reverse order, so 'II' is checked before 'I'. 90 """ 91 if " " in name: # Assume already done 92 return name 93 for number, prefixes in sorted(number_prefixes.items(), reverse=True): 94 for prefix in prefixes: 95 head = name[: len(prefix)] 96 tail = name[len(prefix) :] 97 if head == prefix and tail in unnumbered_book_aliases: 98 return number + " " + tail 99 return re.sub(r"^(\d)([A-Z])([a-z].*)$", r"\1 \2\3", name)
Remove space between any leading digit and all subsequent text.
- '2Tim' becomes '2 Tim'.
- 'SecondTim' or '2ndTim' or 'IITim' becomes '2 Tim' (use number_prefixes).
Note: Must search in reverse order, so 'II' is checked before 'I'.
102def trim_trailing_period(text: str) -> str: 103 """Remove any tailing '.' character.""" 104 return text.rstrip(".")
Remove any tailing '.' character.
107def normalize_spacing(text: str) -> str: 108 """Replace multiple spaces with single spaces. 109 110 Args: 111 text: A string expected to have irregular spacing. 112 """ 113 return re.sub("\\s+", " ", text)
Replace multiple spaces with single spaces.
Arguments:
- text: A string expected to have irregular spacing.
116def pluralize(number: int, singular: str, plural: str = "") -> str: 117 """ 118 Super simple pluralization. 119 """ 120 if number == 1: 121 return "%d %s" % (number, singular) 122 else: 123 if plural != "": 124 return "%d %s" % (number, plural) 125 else: 126 return "%d %ss" % (number, singular)
Super simple pluralization.
129def sequential_replace_tuples(text: str, tuples: list[tuple[str, str]]): 130 """Replace values, searching from the end of each previous replacement. 131 132 This is for replacing matches, and ensures they are replaced in the 133 same order they are matched. 134 135 This function takes find-replace strings as a list of tuples. See 136 `refspy.utils.sequential_replace`. 137 138 Args: 139 text: A string in which matches should be replaces 140 tuples: A list of find-replace tuples, in order of appearance. 141 """ 142 new_text = "" 143 cursor = 0 144 for find, replace in tuples: 145 if find and replace: 146 next_match = text.find(find, cursor) 147 if next_match != -1: 148 new_text += text[cursor:next_match] 149 new_text += replace 150 cursor = next_match + len(find) 151 new_text += text[cursor:] 152 return new_text
Replace values, searching from the end of each previous replacement.
This is for replacing matches, and ensures they are replaced in the same order they are matched.
This function takes find-replace strings as a list of tuples. See
refspy.utils.sequential_replace.
Arguments:
- text: A string in which matches should be replaces
- tuples: A list of find-replace tuples, in order of appearance.
155def sequential_replace(text: str, find: list[str], replace: list[str]) -> str: 156 """Replace values, searching from the end of each previous replacement. 157 158 This function takes find-replace strings as two separate lists. Unmatched 159 entries will be ignored by zip(). See 160 `refspy.utils.sequential_replace_tuples`. 161 162 Args: 163 text: A string in which matches should be replaces 164 find: A list of values to be found, in order of appearance. 165 replace: A list of replacement values. 166 """ 167 return sequential_replace_tuples(text, list(zip(find, replace)))
Replace values, searching from the end of each previous replacement.
This function takes find-replace strings as two separate lists. Unmatched
entries will be ignored by zip(). See
refspy.utils.sequential_replace_tuples.
Arguments:
- text: A string in which matches should be replaces
- find: A list of values to be found, in order of appearance.
- replace: A list of replacement values.
170def string_together(*args: Any) -> str: 171 """Convert objects to strings and concatenate. 172 173 Args: 174 *args: A list of string-convertable values. 175 """ 176 return "".join([str(arg) for arg in args])
Convert objects to strings and concatenate.
Arguments:
- *args: A list of string-convertable values.