Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
beartype / _util / text / utiltextjoin.py
Size: Mime:
#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **string joining utilities** (i.e., callables joining passed
strings into new strings delimited by passed substring delimiters).

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS                            }....................
from beartype.typing import Iterable as typing_Iterable
from beartype._data.hint.datahinttyping import IterableStrs
from collections.abc import (
    Iterable,
    Sequence,
)

# ....................{ JOINERS                            }....................
#FIXME: Unit test the "is_double_quoted" parameter, please.
def join_delimited(
    # Mandatory parameters.
    strs: IterableStrs,

    # Mandatory keyword-only parameters.
    *,
    delimiter_if_two: str,
    delimiter_if_three_or_more_nonlast: str,
    delimiter_if_three_or_more_last: str,

    # Optional keyword-only parameters.
    is_double_quoted: bool = False,
) -> str:
    '''
    Concatenate the passed iterable of zero or more strings delimited by the
    passed delimiter (conditionally depending on both the length of this
    sequence and index of each string in this sequence), yielding a
    human-readable string listing arbitrarily many substrings.

    Specifically, this function returns either:

    * If this iterable contains no strings, the empty string.
    * If this iterable contains one string, this string as is is unmodified.
    * If this iterable contains two strings, these strings delimited by the
      passed ``delimiter_if_two`` delimiter.
    * If this iterable contains three or more strings, a string listing these
      contained strings such that:

      * All contained strings except the last two are suffixed by the passed
        ``delimiter_if_three_or_more_nonlast`` delimiter.
      * The last two contained strings are delimited by the passed
        ``delimiter_if_three_or_more_last`` separator.

    Parameters
    ----------
    strs : Iterable[str]
        Iterable of all strings to be joined.
    delimiter_if_two : str
        Substring separating each string contained in this iterable if this
        iterable contains exactly two strings.
    delimiter_if_three_or_more_nonlast : str
        Substring separating each string *except* the last two contained in
        this iterable if this iterable contains three or more strings.
    delimiter_if_three_or_more_last : str
        Substring separating each string the last two contained in this
        iterable if this iterable contains three or more strings.
    is_double_quoted : bool, optional
        :data:`True` only if **double-quoting** (i.e., both prefixing and
        suffixing by the ``"`` character) each item of this iterable. Defaults
        to :data:`False`.

    Returns
    -------
    str
        Concatenation of these strings.

    Examples
    --------
        >>> join_delimited(
        ...     strs=('Fulgrim', 'Perturabo', 'Angron', 'Mortarion'),
        ...     delimiter_if_two=' and ',
        ...     delimiter_if_three_or_more_nonlast=', ',
        ...     delimiter_if_three_or_more_last=', and ',
        ... )
        'Fulgrim, Perturabo, Angron, and Mortarion'
    '''
    assert isinstance(strs, Iterable) and not isinstance(strs, str), (
        f'{repr(strs)} not non-string iterable.')
    assert isinstance(delimiter_if_two, str), (
        f'{repr(delimiter_if_two)} not string.')
    assert isinstance(delimiter_if_three_or_more_nonlast, str), (
        f'{repr(delimiter_if_three_or_more_nonlast)} not string.')
    assert isinstance(delimiter_if_three_or_more_last, str), (
        f'{repr(delimiter_if_three_or_more_last)} not string.')

    # If this iterable is *NOT* a sequence, internally coerce this iterable
    # into a sequence for subsequent indexing purposes.
    if not isinstance(strs, Sequence):
        strs = tuple(strs)
    # Else, this iterable is already a sequence.
    #
    # In either case, this iterable is now a sequence.

    # If double-quoting these strings, do so.
    if is_double_quoted:
        strs = tuple(f'"{text}"' for text in strs)
    # Else, preserve these strings as is.

    # Number of strings in this sequence.
    num_strs = len(strs)

    # If no strings are passed, return the empty string.
    if num_strs == 0:
        return ''
    # If one string is passed, return this string as is.
    elif num_strs == 1:
        # This is clearly a string, yet mypy thinks it's Any
        return strs[0]  # type: ignore[no-any-return]
    # If two strings are passed, return these strings delimited appropriately.
    elif num_strs == 2:
        return f'{strs[0]}{delimiter_if_two}{strs[1]}'
    # Else, three or more strings are passed.

    # All such strings except the last two, delimited appropriately.
    strs_nonlast = delimiter_if_three_or_more_nonlast.join(strs[0:-2])

    # The last two such strings, delimited appropriately.
    strs_last = f'{strs[-2]}{delimiter_if_three_or_more_last}{strs[-1]}'

    # Return these two substrings, delimited appropriately.
    return f'{strs_nonlast}{delimiter_if_three_or_more_nonlast}{strs_last}'

# ....................{ JOINERS ~ conjunction              }....................
#FIXME: Unit test us up, please.
def join_delimited_conjunction(strs: IterableStrs, **kwargs) -> str:
    '''
    Concatenate the passed iterable of zero or more strings delimited by commas
    and/or the conjunction "and" (conditionally depending on both the length of
    this iterable and index of each string in this iterable), yielding a
    human-readable string listing arbitrarily many substrings conjunctively.

    Specifically, this function returns either:

    * If this iterable contains no strings, the empty string.
    * If this iterable contains one string, this string as is is unmodified.
    * If this iterable contains two strings, these strings delimited by the
      conjunction "and".
    * If this iterable contains three or more strings, a string listing these
      contained strings such that:

      * All contained strings except the last two are suffixed by commas.
      * The last two contained strings are delimited by the conjunction "and".

    Parameters
    ----------
    strs : Iterable[str]
        Iterable of all strings to be concatenated conjunctively.

    All remaining keyword parameters are passed as is to the lower-level
    :func:`.join_delimeted` function underlying this higher-level function.

    Returns
    -------
    str
        Conjunctive concatenation of these strings.
    '''

    # One of us. We accept one-liner. One of us.
    return join_delimited(
        strs=strs,
        delimiter_if_two=' and ',
        delimiter_if_three_or_more_nonlast=', ',
        delimiter_if_three_or_more_last=', and ',
        **kwargs
    )

# ....................{ JOINERS ~ disjunction              }....................
def join_delimited_disjunction(strs: IterableStrs, **kwargs) -> str:
    '''
    Concatenate the passed iterable of zero or more strings delimited by commas
    and/or the disjunction "or" (conditionally depending on both the length of
    this iterable and index of each string in this iterable), yielding a
    human-readable string listing arbitrarily many substrings disjunctively.

    Specifically, this function returns either:

    * If this iterable contains no strings, the empty string.
    * If this iterable contains one string, this string as is is unmodified.
    * If this iterable contains two strings, these strings delimited by the
      disjunction "or".
    * If this iterable contains three or more strings, a string listing these
      contained strings such that:

      * All contained strings except the last two are suffixed by commas.
      * The last two contained strings are delimited by the disjunction "or".

    Parameters
    ----------
    strs : Iterable[str]
        Iterable of all strings to be concatenated disjunctively.

    All remaining keyword parameters are passed as is to the lower-level
    :func:`.join_delimeted` function underlying this higher-level function.

    Returns
    -------
    str
        Disjunctive concatenation of these strings.
    '''

    # He will join us... OR DIE! *cackling heard*
    return join_delimited(
        strs=strs,
        delimiter_if_two=' or ',
        delimiter_if_three_or_more_nonlast=', ',
        delimiter_if_three_or_more_last=', or ',
        **kwargs
    )


def join_delimited_disjunction_types(types: typing_Iterable[type]) -> str:
    '''
    Concatenate the human-readable classname of each class in the passed
    iterable delimited by commas and/or the disjunction "or" (conditionally
    depending on both the length of this iterable and index of each string in
    this iterable), yielding a human-readable string listing arbitrarily many
    classnames disjunctively.

    Parameters
    ----------
    types : Iterable[type]
        Iterable of all classes whose human-readable classnames are to be
        concatenated disjunctively.

    Returns
    -------
    str
        Disjunctive concatenation of these classnames.
    '''

    # Avoid circular import dependencies.
    from beartype._util.text.utiltextlabel import label_type

    # Make it so, ensign.
    return join_delimited_disjunction(label_type(cls) for cls in types)