def counting_sort(array, k, key_func=lambda x: x, ascending=True):
    """Sort array using counting sort algorithm.

    Parameters
    ----------
    array : list
        List to be sorted; Must produce integer values in the range 0 to k-1
        when key_func is applied to its elements
    k : int
        Max value of key_func(i), -1, where i an element of array
    key_func : func, optional
        Function to apply to elements of array to produce an integer in range 0
        to k-1
    ascending : bool, optional
        If True sort array from smallest to largest; False -> sort array from
        largest to smallest

    Returns
    -------
    output_arr : list
        Input array sorted.
    """
    # Create copy to avoid modifying array inplace
    array = array.copy()

    # Generate array to contain counts of each distinct value in array
    counts = [0] * k

    # Populate counts array by running through array
    for item in array:
        counts[key_func(item)] += 1

    # Calculate starting index for each k value, putting them in counts
    # Effectively storing number of items with key_func(item) less than i
    total = 0
    for i in range(k):
        old_count = counts[i]
        counts[i] = total
        total += old_count

    # Transfer to output array
    output_arr = [None] * len(array)
    for item in array:
        # Store item in correct position in array
        output_arr[counts[key_func(item)]] = item
        # Increment index for relevant k value
        counts[key_func(item)] += 1

    if ascending:
        return output_arr
    else:
        # Reverse array for descending order sort
        return output_arr[::-1]