Update sphinx links to anonymous references to work around a warning
[pyutils.git] / src / pyutils / string_utils.py
index f82ec4b5e7887ff9a22131de5ab708f7ce8fdbb0..d397ad8e0b306c9494168cf859b2855ab0604045 100644 (file)
@@ -25,9 +25,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 
-This class is based on: https://github.com/daveoncode/python-string-utils.
-See NOTICE in the root of this module for a detailed enumeration of what
-work is Davide's and what work was added by Scott.
+This class is based on:
+https://github.com/daveoncode/python-string-utils.  See `NOTICE
+<https://wannabe.guru.org/gitweb/?p=pyutils.git;a=blob_plain;f=NOTICE;hb=HEAD>`__
+in the root of this module for a detailed enumeration of what work is
+Davide's and what work was added by Scott.
+
 """
 
 import base64
@@ -213,7 +216,14 @@ TENS_WORDS = [
     "ninety",
 ]
 
-scales = ["hundred", "thousand", "million", "billion", "trillion", "quadrillion"]
+MAGNITUDE_SCALES = [
+    "hundred",
+    "thousand",
+    "million",
+    "billion",
+    "trillion",
+    "quadrillion",
+]
 
 NUM_WORDS = {}
 NUM_WORDS["and"] = (1, 0)
@@ -221,7 +231,7 @@ for i, word in enumerate(UNIT_WORDS):
     NUM_WORDS[word] = (1, i)
 for i, word in enumerate(TENS_WORDS):
     NUM_WORDS[word] = (1, i * 10)
-for i, word in enumerate(scales):
+for i, word in enumerate(MAGNITUDE_SCALES):
     if i == 0:
         NUM_WORDS[word] = (100, 0)
     else:
@@ -238,6 +248,8 @@ def is_none_or_empty(in_str: Optional[str]) -> bool:
         True if the input string is either None or an empty string,
         False otherwise.
 
+    See also :meth:`is_string` and :meth:`is_empty_string`.
+
     >>> is_none_or_empty("")
     True
     >>> is_none_or_empty(None)
@@ -258,6 +270,8 @@ def is_string(obj: Any) -> bool:
     Returns:
         True if the object is a string and False otherwise.
 
+    See also :meth:`is_empty_string`, :meth:`is_none_or_empty`.
+
     >>> is_string('test')
     True
     >>> is_string(123)
@@ -277,6 +291,8 @@ def is_empty_string(in_str: Any) -> bool:
 
     Returns:
         True if the string is empty and False otherwise.
+
+    See also :meth:`is_none_or_empty`, :meth:`is_full_string`.
     """
     return is_empty(in_str)
 
@@ -289,6 +305,8 @@ def is_empty(in_str: Any) -> bool:
     Returns:
         True if the string is empty and false otherwise.
 
+    See also :meth:`is_none_or_empty`, :meth:`is_full_string`.
+
     >>> is_empty('')
     True
     >>> is_empty('    \t\t    ')
@@ -312,6 +330,8 @@ def is_full_string(in_str: Any) -> bool:
         True if the object is a string and is not empty ('') and
         is not only composed of whitespace.
 
+    See also :meth:`is_string`, :meth:`is_empty_string`, :meth:`is_none_or_empty`.
+
     >>> is_full_string('test!')
     True
     >>> is_full_string('')
@@ -335,6 +355,10 @@ def is_number(in_str: str) -> bool:
         True if the string contains a valid numberic value and
         False otherwise.
 
+    See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
+    :meth:`is_hexidecimal_integer_number`, :meth:`is_octal_integer_number`,
+    etc...
+
     >>> is_number(100.5)
     Traceback (most recent call last):
     ...
@@ -365,6 +389,10 @@ def is_integer_number(in_str: str) -> bool:
         decimal, hex, or octal, regular or scientific) integral
         expression and False otherwise.
 
+    See also :meth:`is_number`, :meth:`is_decimal_number`,
+    :meth:`is_hexidecimal_integer_number`, :meth:`is_octal_integer_number`,
+    etc...
+
     >>> is_integer_number('42')
     True
     >>> is_integer_number('42.0')
@@ -386,6 +414,9 @@ def is_hexidecimal_integer_number(in_str: str) -> bool:
     Returns:
         True if the string is a hex integer number and False otherwise.
 
+    See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
+    :meth:`is_octal_integer_number`, :meth:`is_binary_integer_number`, etc...
+
     >>> is_hexidecimal_integer_number('0x12345')
     True
     >>> is_hexidecimal_integer_number('0x1A3E')
@@ -422,6 +453,10 @@ def is_octal_integer_number(in_str: str) -> bool:
     Returns:
         True if the string is a valid octal integral number and False otherwise.
 
+    See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
+    :meth:`is_hexidecimal_integer_number`, :meth:`is_binary_integer_number`,
+    etc...
+
     >>> is_octal_integer_number('0o777')
     True
     >>> is_octal_integer_number('-0O115')
@@ -446,6 +481,10 @@ def is_binary_integer_number(in_str: str) -> bool:
     Returns:
         True if the string contains a binary integral number and False otherwise.
 
+    See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
+    :meth:`is_hexidecimal_integer_number`, :meth:`is_octal_integer_number`,
+    etc...
+
     >>> is_binary_integer_number('0b10111')
     True
     >>> is_binary_integer_number('-0b111')
@@ -472,8 +511,18 @@ def to_int(in_str: str) -> int:
     Returns:
         The integral value of the string or raises on error.
 
+    See also :meth:`is_integer_number`, :meth:`is_decimal_number`,
+    :meth:`is_hexidecimal_integer_number`, :meth:`is_octal_integer_number`,
+    :meth:`is_binary_integer_number`, etc...
+
     >>> to_int('1234')
     1234
+    >>> to_int('0x1234')
+    4660
+    >>> to_int('0b01101')
+    13
+    >>> to_int('0o777')
+    511
     >>> to_int('test')
     Traceback (most recent call last):
     ...
@@ -493,6 +542,18 @@ def to_int(in_str: str) -> int:
 def number_string_to_integer(in_str: str) -> int:
     """Convert a string containing a written-out number into an int.
 
+    Args:
+        in_str: the string containing the long-hand written out integer number
+            in English.  See examples below.
+
+    Returns:
+        The integer whose value was parsed from in_str.
+
+    See also :meth:`integer_to_number_string`.
+
+    .. warning::
+        This code only handles integers; it will not work with decimals / floats.
+
     >>> number_string_to_integer("one hundred fifty two")
     152
 
@@ -508,7 +569,7 @@ def number_string_to_integer(in_str: str) -> int:
     ValueError: Unknown word: xyzzy
     """
     if type(in_str) == int:
-        return in_str
+        return int(in_str)
 
     current = result = 0
     in_str = in_str.replace('-', ' ')
@@ -529,8 +590,19 @@ def number_string_to_integer(in_str: str) -> int:
 
 def integer_to_number_string(num: int) -> str:
     """
-    Opposite of number_string_to_integer; convert a number to a written out
-    longhand format.
+    Opposite of :meth:`number_string_to_integer`; converts a number to a written out
+    longhand format in English.
+
+    Args:
+        num: the integer number to convert
+
+    Returns:
+        The long-hand written out English form of the number.  See examples below.
+
+    See also :meth:`number_string_to_integer`.
+
+    .. warning::
+        This method does not handle decimals or floats, only ints.
 
     >>> integer_to_number_string(9)
     'nine'
@@ -540,7 +612,6 @@ def integer_to_number_string(num: int) -> str:
 
     >>> integer_to_number_string(123219982)
     'one hundred twenty three million two hundred nineteen thousand nine hundred eighty two'
-
     """
 
     if num < 20:
@@ -583,6 +654,8 @@ def is_decimal_number(in_str: str) -> bool:
         otherwise.  A decimal may be signed or unsigned or use
         a "scientific notation".
 
+    See also :meth:`is_integer_number`.
+
     .. note::
         We do not consider integers without a decimal point
         to be decimals; they return False (see example).
@@ -603,6 +676,8 @@ def strip_escape_sequences(in_str: str) -> str:
     Returns:
         in_str with escape sequences removed.
 
+    See also: :mod:`pyutils.ansi`.
+
     .. note::
         What is considered to be an "escape sequence" is defined
         by a regular expression.  While this gets common ones,
@@ -647,6 +722,7 @@ def add_thousands_separator(in_str: str, *, separator_char=',', places=3) -> str
 
 
 def _add_thousands_separator(in_str: str, *, separator_char=',', places=3) -> str:
+    """Internal helper"""
     decimal_part = ""
     if '.' in in_str:
         (in_str, decimal_part) = in_str.split('.')
@@ -743,6 +819,8 @@ def suffix_string_to_number(in_str: str) -> Optional[int]:
     Returns:
         An integer number of bytes or None to indicate an error.
 
+    See also :meth:`number_to_suffix_string`.
+
     >>> suffix_string_to_number('1Mb')
     1048576
     >>> suffix_string_to_number('13.1Gb')
@@ -784,6 +862,8 @@ def number_to_suffix_string(num: int) -> Optional[str]:
         A string with a suffix representing num bytes concisely or
         None to indicate an error.
 
+    See also: :meth:`suffix_string_to_number`.
+
     >>> number_to_suffix_string(14066017894)
     '13.1Gb'
     >>> number_to_suffix_string(1024 * 1024)
@@ -821,6 +901,13 @@ def is_credit_card(in_str: Any, card_type: str = None) -> bool:
 
     Returns:
         True if in_str is a valid credit card number.
+
+    .. warning::
+        This code is not verifying the authenticity of the credit card (i.e.
+        not checking whether it's a real card that can be charged); rather
+        it's only checking that the number follows the "rules" for numbering
+        established by credit card issuers.
+
     """
     if not is_full_string(in_str):
         return False
@@ -849,6 +936,8 @@ def is_camel_case(in_str: Any) -> bool:
         * it's composed only by letters ([a-zA-Z]) and optionally numbers ([0-9])
         * it contains both lowercase and uppercase letters
         * it does not start with a number
+
+    See also :meth:`is_snake_case`, :meth:`is_slug`, and :meth:`camel_case_to_snake_case`.
     """
     return is_full_string(in_str) and CAMEL_CASE_TEST_RE.match(in_str) is not None
 
@@ -865,6 +954,8 @@ def is_snake_case(in_str: Any, *, separator: str = "_") -> bool:
         * it contains at least one underscore (or provided separator)
         * it does not start with a number
 
+    See also :meth:`is_camel_case`, :meth:`is_slug`, and :meth:`snake_case_to_camel_case`.
+
     >>> is_snake_case('this_is_a_test')
     True
     >>> is_snake_case('___This_Is_A_Test_1_2_3___')
@@ -916,6 +1007,8 @@ def is_uuid(in_str: Any, allow_hex: bool = False) -> bool:
     Returns:
         True if the in_str contains a valid UUID and False otherwise.
 
+    See also :meth:`generate_uuid`.
+
     >>> is_uuid('6f8aa2f9-686c-4ac3-8766-5712354a04cf')
     True
     >>> is_uuid('6f8aa2f9686c4ac387665712354a04cf')
@@ -938,6 +1031,9 @@ def is_ip_v4(in_str: Any) -> bool:
     Returns:
         True if in_str contains a valid IPv4 address and False otherwise.
 
+    See also :meth:`extract_ip_v4`, :meth:`is_ip_v6`, :meth:`extract_ip_v6`,
+    and :meth:`is_ip`.
+
     >>> is_ip_v4('255.200.100.75')
     True
     >>> is_ip_v4('nope')
@@ -964,6 +1060,9 @@ def extract_ip_v4(in_str: Any) -> Optional[str]:
         The first extracted IPv4 address from in_str or None if
         none were found or an error occurred.
 
+    See also :meth:`is_ip_v4`, :meth:`is_ip_v6`, :meth:`extract_ip_v6`,
+    and :meth:`is_ip`.
+
     >>> extract_ip_v4('   The secret IP address: 127.0.0.1 (use it wisely)   ')
     '127.0.0.1'
     >>> extract_ip_v4('Your mom dresses you funny.')
@@ -984,6 +1083,9 @@ def is_ip_v6(in_str: Any) -> bool:
     Returns:
         True if in_str contains a valid IPv6 address and False otherwise.
 
+    See also :meth:`is_ip_v4`, :meth:`extract_ip_v4`, :meth:`extract_ip_v6`,
+    and :meth:`is_ip`.
+
     >>> is_ip_v6('2001:db8:85a3:0000:0000:8a2e:370:7334')
     True
     >>> is_ip_v6('2001:db8:85a3:0000:0000:8a2e:370:?')    # invalid "?"
@@ -1001,6 +1103,9 @@ def extract_ip_v6(in_str: Any) -> Optional[str]:
         The first IPv6 address found in in_str or None if no address
         was found or an error occurred.
 
+    See also :meth:`is_ip_v4`, :meth:`is_ip_v6`, :meth:`extract_ip_v4`,
+    and :meth:`is_ip`.
+
     >>> extract_ip_v6('IP: 2001:db8:85a3:0000:0000:8a2e:370:7334')
     '2001:db8:85a3:0000:0000:8a2e:370:7334'
     >>> extract_ip_v6("(and she's ugly too, btw)")
@@ -1022,6 +1127,9 @@ def is_ip(in_str: Any) -> bool:
         True if in_str contains a valid IP address (either IPv4 or
         IPv6).
 
+    See also :meth:`is_ip_v4`, :meth:`is_ip_v6`, :meth:`extract_ip_v6`,
+    and :meth:`extract_ip_v4`.
+
     >>> is_ip('255.200.100.75')
     True
     >>> is_ip('2001:db8:85a3:0000:0000:8a2e:370:7334')
@@ -1043,6 +1151,9 @@ def extract_ip(in_str: Any) -> Optional[str]:
         The first IP address (IPv4 or IPv6) found in in_str or
         None to indicate none found or an error condition.
 
+    See also :meth:`is_ip_v4`, :meth:`is_ip_v6`, :meth:`extract_ip_v6`,
+    and :meth:`extract_ip_v4`.
+
     >>> extract_ip('Attacker: 255.200.100.75')
     '255.200.100.75'
     >>> extract_ip('Remote host: 2001:db8:85a3:0000:0000:8a2e:370:7334')
@@ -1063,6 +1174,8 @@ def is_mac_address(in_str: Any) -> bool:
     Returns:
         True if in_str is a valid MAC address False otherwise.
 
+    See also :meth:`extract_mac_address`, :meth:`is_ip`, etc...
+
     >>> is_mac_address("34:29:8F:12:0D:2F")
     True
     >>> is_mac_address('34:29:8f:12:0d:2f')
@@ -1084,6 +1197,8 @@ def extract_mac_address(in_str: Any, *, separator: str = ":") -> Optional[str]:
         The first MAC address found in in_str or None to indicate no
         match or an error.
 
+    See also :meth:`is_mac_address`, :meth:`is_ip`, and :meth:`extract_ip`.
+
     >>> extract_mac_address(' MAC Address: 34:29:8F:12:0D:2F')
     '34:29:8F:12:0D:2F'
 
@@ -1110,6 +1225,8 @@ def is_slug(in_str: Any, separator: str = "-") -> bool:
     Returns:
         True if in_str is a slug string and False otherwise.
 
+    See also :meth:`is_camel_case`, :meth:`is_snake_case`, and :meth:`slugify`.
+
     >>> is_slug('my-blog-post-title')
     True
     >>> is_slug('My blog post title')
@@ -1130,6 +1247,8 @@ def contains_html(in_str: str) -> bool:
         True if the given string contains HTML/XML tags and False
         otherwise.
 
+    See also :meth:`strip_html`.
+
     .. warning::
         By design, this function matches ANY type of tag, so don't expect
         to use it as an HTML validator.  It's a quick sanity check at
@@ -1156,7 +1275,6 @@ def words_count(in_str: str) -> int:
         The number of words contained in the given string.
 
     .. note::
-
         This method is "smart" in that it does consider only sequences
         of one or more letter and/or numbers to be "words".  Thus a
         string like this: "! @ # % ... []" will return zero.  Moreover
@@ -1183,7 +1301,6 @@ def word_count(in_str: str) -> int:
         The number of words contained in the given string.
 
     .. note::
-
         This method is "smart" in that it does consider only sequences
         of one or more letter and/or numbers to be "words".  Thus a
         string like this: "! @ # % ... []" will return zero.  Moreover
@@ -1208,6 +1325,8 @@ def generate_uuid(omit_dashes: bool = False) -> str:
         A generated UUID string (using `uuid.uuid4()`) with or without
         dashes per the omit_dashes arg.
 
+    See also :meth:`is_uuid`, :meth:`generate_random_alphanumeric_string`.
+
     generate_uuid() # possible output: '97e3a716-6b33-4ab9-9bb1-8128cb24d76b'
     generate_uuid(omit_dashes=True) # possible output: '97e3a7166b334ab99bb18128cb24d76b'
     """
@@ -1226,6 +1345,8 @@ def generate_random_alphanumeric_string(size: int) -> str:
         A string of the specified size containing random characters
         (uppercase/lowercase ascii letters and digits).
 
+    See also :meth:`asciify`, :meth:`generate_uuid`.
+
     >>> random.seed(22)
     >>> generate_random_alphanumeric_string(9)
     '96ipbNClS'
@@ -1263,6 +1384,8 @@ def camel_case_to_snake_case(in_str, *, separator="_"):
         original string if it is not a valid camel case string or some
         other error occurs.
 
+    See also :meth:`is_camel_case`, :meth:`is_snake_case`, and :meth:`is_slug`.
+
     >>> camel_case_to_snake_case('MacAddressExtractorFactory')
     'mac_address_extractor_factory'
     >>> camel_case_to_snake_case('Luke Skywalker')
@@ -1287,6 +1410,8 @@ def snake_case_to_camel_case(
         provided or the original string back again if it is not valid
         snake case or another error occurs.
 
+    See also :meth:`is_camel_case`, :meth:`is_snake_case`, and :meth:`is_slug`.
+
     >>> snake_case_to_camel_case('this_is_a_test')
     'ThisIsATest'
     >>> snake_case_to_camel_case('Han Solo')
@@ -1310,6 +1435,8 @@ def to_char_list(in_str: str) -> List[str]:
     Returns:
         A list of strings of length one each.
 
+    See also :meth:`from_char_list`.
+
     >>> to_char_list('test')
     ['t', 'e', 's', 't']
     """
@@ -1327,6 +1454,8 @@ def from_char_list(in_list: List[str]) -> str:
         The string resulting from gluing the characters in in_list
         together.
 
+    See also :meth:`to_char_list`.
+
     >>> from_char_list(['t', 'e', 's', 't'])
     'test'
     """
@@ -1366,6 +1495,8 @@ def scramble(in_str: str) -> Optional[str]:
         in the same original string as no check is done.  Returns
         None to indicate error conditions.
 
+    See also :mod:`pyutils.unscrambler`.
+
     >>> random.seed(22)
     >>> scramble('awesome')
     'meosaew'
@@ -1383,6 +1514,8 @@ def strip_html(in_str: str, keep_tag_content: bool = False) -> str:
         A string with all HTML tags removed (optionally with tag contents
         preserved).
 
+    See also :meth:`contains_html`.
+
     .. note::
         This method uses simple regular expressions to strip tags and is
         not a full fledged HTML parser by any means.  Consider using
@@ -1411,6 +1544,8 @@ def asciify(in_str: str) -> str:
         by translating all non-ascii chars into their closest possible
         ASCII representation (eg: ó -> o, Ë -> E, ç -> c...).
 
+    See also :meth:`to_ascii`, :meth:`generate_random_alphanumeric_string`.
+
     .. warning::
         Some chars may be lost if impossible to translate.
 
@@ -1449,6 +1584,8 @@ def slugify(in_str: str, *, separator: str = "-") -> str:
         * all chars are encoded as ascii (by using :meth:`asciify`)
         * is safe for URL
 
+    See also :meth:`is_slug` and :meth:`asciify`.
+
     >>> slugify('Top 10 Reasons To Love Dogs!!!')
     'top-10-reasons-to-love-dogs'
     >>> slugify('Mönstér Mägnët')
@@ -1487,6 +1624,8 @@ def to_bool(in_str: str) -> bool:
 
         Otherwise False is returned.
 
+    See also :mod:`pyutils.argparse_utils`.
+
     >>> to_bool('True')
     True
 
@@ -1520,6 +1659,9 @@ def to_date(in_str: str) -> Optional[datetime.date]:
         an error.  This parser is relatively clever; see
         :class:`datetimez.dateparse_utils` docs for details.
 
+    See also: :mod:`pyutils.datetimez.dateparse_utils`, :meth:`extract_date`,
+    :meth:`is_valid_date`, :meth:`to_datetime`, :meth:`valid_datetime`.
+
     >>> to_date('9/11/2001')
     datetime.date(2001, 9, 11)
     >>> to_date('xyzzy')
@@ -1545,6 +1687,9 @@ def extract_date(in_str: Any) -> Optional[datetime.datetime]:
     Returns:
         a datetime if date was found, otherwise None
 
+    See also: :mod:`pyutils.datetimez.dateparse_utils`, :meth:`to_date`,
+    :meth:`is_valid_date`, :meth:`to_datetime`, :meth:`valid_datetime`.
+
     >>> extract_date("filename.txt    dec 13, 2022")
     datetime.datetime(2022, 12, 13, 0, 0)
 
@@ -1583,6 +1728,9 @@ def is_valid_date(in_str: str) -> bool:
         and False otherwise.  This parser is relatively clever; see
         :class:`datetimez.dateparse_utils` docs for details.
 
+    See also: :mod:`pyutils.datetimez.dateparse_utils`, :meth:`to_date`,
+    :meth:`extract_date`, :meth:`to_datetime`, :meth:`valid_datetime`.
+
     >>> is_valid_date('1/2/2022')
     True
     >>> is_valid_date('christmas')
@@ -1614,6 +1762,9 @@ def to_datetime(in_str: str) -> Optional[datetime.datetime]:
         an error.  This parser is relatively clever; see
         :class:`datetimez.dateparse_utils` docs for details.
 
+    See also: :mod:`pyutils.datetimez.dateparse_utils`, :meth:`to_date`,
+    :meth:`extract_date`, :meth:`valid_datetime`.
+
     >>> to_datetime('7/20/1969 02:56 GMT')
     datetime.datetime(1969, 7, 20, 2, 56, tzinfo=<StaticTzInfo 'GMT'>)
     """
@@ -1689,9 +1840,7 @@ def dedent(in_str: str) -> Optional[str]:
     Returns:
         A string with tab indentation removed or None on error.
 
-    .. note::
-
-        Inspired by analogous Scala function.
+    See also :meth:`indent`.
 
     >>> dedent('\t\ttest\\n\t\ting')
     'test\\ning'
@@ -1712,6 +1861,8 @@ def indent(in_str: str, amount: int) -> str:
     Returns:
         An indented string created by prepending amount spaces.
 
+    See also :meth:`dedent`.
+
     >>> indent('This is a test', 4)
     '    This is a test'
     """
@@ -1722,16 +1873,8 @@ def indent(in_str: str, amount: int) -> str:
     return line_separator.join(lines)
 
 
-def sprintf(*args, **kwargs) -> str:
-    """
-    Args:
-        This function uses the same syntax as the builtin print
-        function.
-
-    Returns:
-        An interpolated string capturing print output, like man(3)
-        `sprintf`.
-    """
+def _sprintf(*args, **kwargs) -> str:
+    """Internal helper."""
     ret = ""
 
     sep = kwargs.pop("sep", None)
@@ -1770,6 +1913,8 @@ def strip_ansi_sequences(in_str: str) -> str:
     Returns:
         in_str with recognized ANSI escape sequences removed.
 
+    See also :mod:`pyutils.ansi`.
+
     .. warning::
         This method works by using a regular expression.
         It works for all ANSI escape sequences I've tested with but
@@ -1800,7 +1945,6 @@ class SprintfStdout(contextlib.AbstractContextManager):
     >>> print(buf(), end='')
     test
     1, 2, 3
-
     """
 
     def __init__(self) -> None:
@@ -1830,7 +1974,6 @@ def capitalize_first_letter(in_str: str) -> str:
     'Test'
     >>> capitalize_first_letter("ALREADY!")
     'ALREADY!'
-
     """
     return in_str[0].upper() + in_str[1:]
 
@@ -1843,6 +1986,9 @@ def it_they(n: int) -> str:
     Returns:
         'it' if n is one or 'they' otherwize.
 
+    See also :meth:`is_are`, :meth:`pluralize`, :meth:`make_contractions`,
+    :meth:`thify`.
+
     Suggested usage::
 
         n = num_files_saved_to_tmp()
@@ -1867,6 +2013,9 @@ def is_are(n: int) -> str:
     Returns:
         'is' if n is one or 'are' otherwize.
 
+    See also :meth:`it_they`, :meth:`pluralize`, :meth:`make_contractions`,
+    :meth:`thify`.
+
     Suggested usage::
 
         n = num_files_saved_to_tmp()
@@ -1892,6 +2041,9 @@ def pluralize(n: int) -> str:
     Returns:
         's' if n is greater than one otherwize ''.
 
+    See also :meth:`it_they`, :meth:`is_are`, :meth:`make_contractions`,
+    :meth:`thify`.
+
     Suggested usage::
 
         n = num_files_saved_to_tmp()
@@ -1923,6 +2075,8 @@ def make_contractions(txt: str) -> str:
         Output text identical to original input except for any
         recognized contractions are formed.
 
+    See also :meth:`it_they`, :meth:`is_are`, :meth:`make_contractions`.
+
     .. note::
         The order in which we create contractions is defined by the
         implementation and what I thought made more sense when writing
@@ -2026,6 +2180,8 @@ def thify(n: int) -> str:
     Returns:
         The proper cardinal suffix for a number.
 
+    See also :meth:`it_they`, :meth:`is_are`, :meth:`make_contractions`.
+
     Suggested usage::
 
         attempt_count = 0
@@ -2064,6 +2220,8 @@ def ngrams(txt: str, n: int):
     Returns:
         Generates the ngrams from the input string.
 
+    See also :meth:`ngrams_presplit`, :meth:`bigrams`, :meth:`trigrams`.
+
     >>> [x for x in ngrams('This is a test', 2)]
     ['This is', 'is a', 'a test']
     """
@@ -2078,6 +2236,8 @@ def ngrams(txt: str, n: int):
 def ngrams_presplit(words: Sequence[str], n: int):
     """
     Same as :meth:`ngrams` but with the string pre-split.
+
+    See also :meth:`ngrams`, :meth:`bigrams`, :meth:`trigrams`.
     """
     return list_utils.ngrams(words, n)
 
@@ -2085,6 +2245,8 @@ def ngrams_presplit(words: Sequence[str], n: int):
 def bigrams(txt: str):
     """Generates the bigrams (n=2) of the given string.
 
+    See also :meth:`ngrams`, :meth:`trigrams`.
+
     >>> [x for x in bigrams('this is a test')]
     ['this is', 'is a', 'a test']
     """
@@ -2092,7 +2254,10 @@ def bigrams(txt: str):
 
 
 def trigrams(txt: str):
-    """Generates the trigrams (n=3) of the given string."""
+    """Generates the trigrams (n=3) of the given string.
+
+    See also :meth:`ngrams`, :meth:`bigrams`.
+    """
     return ngrams(txt, 3)
 
 
@@ -2116,6 +2281,8 @@ def shuffle_columns_into_list(
         A list of string created by following the instructions set forth
         in column_specs.
 
+    See also :meth:`shuffle_columns_into_dict`.
+
     >>> cols = '-rwxr-xr-x 1 scott wheel 3.1K Jul  9 11:34 acl_test.py'.split()
     >>> shuffle_columns_into_list(
     ...     cols,
@@ -2158,6 +2325,8 @@ def shuffle_columns_into_dict(
     Returns:
         A dict formed by applying the column_specs instructions.
 
+    See also :meth:`shuffle_columns_into_list`, :meth:`interpolate_using_dict`.
+
     >>> cols = '-rwxr-xr-x 1 scott wheel 3.1K Jul  9 11:34 acl_test.py'.split()
     >>> shuffle_columns_into_dict(
     ...     cols,
@@ -2187,11 +2356,13 @@ def interpolate_using_dict(txt: str, values: Dict[str, str]) -> str:
         txt: the mad libs template
         values: what you and your kids chose for each category.
 
+    See also :meth:`shuffle_columns_into_list`, :meth:`shuffle_columns_into_dict`.
+
     >>> interpolate_using_dict('This is a {adjective} {noun}.',
     ...                        {'adjective': 'good', 'noun': 'example'})
     'This is a good example.'
     """
-    return sprintf(txt.format(**values), end='')
+    return _sprintf(txt.format(**values), end='')
 
 
 def to_ascii(txt: str):
@@ -2202,6 +2373,9 @@ def to_ascii(txt: str):
     Returns:
         txt encoded as an ASCII byte string.
 
+    See also :meth:`to_base64`, :meth:`to_bitstring`, :meth:`to_bytes`,
+    :meth:`generate_random_alphanumeric_string`, :meth:`asciify`.
+
     >>> to_ascii('test')
     b'test'
 
@@ -2224,6 +2398,9 @@ def to_base64(txt: str, *, encoding='utf-8', errors='surrogatepass') -> bytes:
         txt encoded with a 64-chracter alphabet.  Similar to and compatible
         with uuencode/uudecode.
 
+    See also :meth:`is_base64`, :meth:`to_ascii`, :meth:`to_bitstring`,
+    :meth:`from_base64`.
+
     >>> to_base64('hello?')
     b'aGVsbG8/\\n'
     """
@@ -2240,6 +2417,8 @@ def is_base64(txt: str) -> bool:
         txt was encoded with Python's standard base64 alphabet which
         is the same as what uuencode/uudecode uses).
 
+    See also :meth:`to_base64`, :meth:`from_base64`.
+
     >>> is_base64('test')    # all letters in the b64 alphabet
     True
 
@@ -2267,6 +2446,8 @@ def from_base64(b64: bytes, encoding='utf-8', errors='surrogatepass') -> str:
         The decoded form of b64 as a normal python string.  Similar to
         and compatible with uuencode / uudecode.
 
+    See also :meth:`to_base64`, :meth:`is_base64`.
+
     >>> from_base64(b'aGVsbG8/\\n')
     'hello?'
     """
@@ -2304,6 +2485,9 @@ def to_bitstring(txt: str, *, delimiter='') -> str:
     Returns:
         txt converted to ascii/binary and then chopped into bytes.
 
+    See also :meth:`to_base64`, :meth:`from_bitstring`, :meth:`is_bitstring`,
+    :meth:`chunk`.
+
     >>> to_bitstring('hello?')
     '011010000110010101101100011011000110111100111111'
 
@@ -2329,6 +2513,9 @@ def is_bitstring(txt: str) -> bool:
         Note that if delimiter is non empty this code will not
         recognize the bitstring.
 
+    See also :meth:`to_base64`, :meth:`from_bitstring`, :meth:`to_bitstring`,
+    :meth:`chunk`.
+
     >>> is_bitstring('011010000110010101101100011011000110111100111111')
     True
 
@@ -2348,6 +2535,9 @@ def from_bitstring(bits: str, encoding='utf-8', errors='surrogatepass') -> str:
         The regular python string represented by bits.  Note that this
         code does not work with to_bitstring when delimiter is non-empty.
 
+    See also :meth:`to_base64`, :meth:`to_bitstring`, :meth:`is_bitstring`,
+    :meth:`chunk`.
+
     >>> from_bitstring('011010000110010101101100011011000110111100111111')
     'hello?'
     """
@@ -2365,6 +2555,8 @@ def ip_v4_sort_key(txt: str) -> Optional[Tuple[int, ...]]:
         IP addresses using a normal comparator will do something sane
         and desireable.
 
+    See also :meth:`is_ip_v4`.
+
     >>> ip_v4_sort_key('10.0.0.18')
     (10, 0, 0, 18)
 
@@ -2388,6 +2580,8 @@ def path_ancestors_before_descendants_sort_key(volume: str) -> Tuple[str, ...]:
         volumes using a normal comparator will do something sane
         and desireable.
 
+    See also :mod:`pyutils.files.file_utils`.
+
     >>> path_ancestors_before_descendants_sort_key('/usr/local/bin')
     ('usr', 'local', 'bin')
 
@@ -2408,6 +2602,8 @@ def replace_all(in_str: str, replace_set: str, replacement: str) -> str:
         replacement: the character to replace any member of replace_set
             with
 
+    See also :meth:`replace_nth`.
+
     Returns:
         The string with replacements executed.
 
@@ -2430,6 +2626,8 @@ def replace_nth(in_str: str, source: str, target: str, nth: int):
         target: the replacement text
         nth: which occurrance of source to replace?
 
+    See also :meth:`replace_all`.
+
     >>> replace_nth('this is a test', ' ', '-', 3)
     'this is a-test'
     """