import six


def range_join(numbers, to_str=False, sep=",", range_sep=":"):
    """
    Takes a sequence of positive integer numbers given either as integer or string types, and
    returns a sequence 1- and 2-tuples, denoting either single numbers or inclusive start and stop
    values of possible ranges. When *to_str* is *True*, a string is returned in a format consistent
    to :py:func:`range_expand` with ranges constructed by *range_sep* and merged with *sep*.
    Example:

    .. code-block:: python

        range_join([1, 2, 3, 5])
        # -> [(1, 3), (5,)]

        range_join([1, 2, 3, 5, 7, 8, 9])
        # -> [(1, 3), (5,), (7, 9)]

        range_join([1, 2, 3, 5, 7, 8, 9], to_str=True)
        # -> "1:3,5,7:9"
    """
    if not numbers:
        return "" if to_str else []

    # check type, convert, make unique and sort
    _numbers = []
    for n in numbers:
        if isinstance(n, six.string_types):
            try:
                n = int(n)
            except ValueError:
                raise ValueError("invalid number format '{}'".format(n))
        if isinstance(n, six.integer_types):
            _numbers.append(n)
        else:
            raise TypeError("cannot handle non-integer value '{}' in numbers to join".format(n))
    numbers = sorted(set(_numbers))

    # iterate through numbers, keep track of last starts and stops and fill a list of range tuples
    ranges = []
    start = stop = numbers[0]
    for n in numbers[1:]:
        if n == stop + 1:
            stop += 1
        else:
            ranges.append((start,) if start == stop else (start, stop))
            start = stop = n
    ranges.append((start,) if start == stop else (start, stop))

    # convert to string representation
    if to_str:
        ranges = sep.join(
            (str(r[0]) if len(r) == 1 else "{1}{0}{2}".format(range_sep, *r))
            for r in ranges
        )

    return ranges