# Sort examples

# 1. Basic sort

# The in-built function sorted()
# does not change the original list and returns
# its sorted copy

print( 'function sorted():' )
L1 = [6, -2, 5, 0, 12, 5]
sortedL1  = sorted( L1 )
print( L1 ); print( sortedL1 )
print()

# The function sort() is a method of any list,
# it sorts the list nad changes the order of items in it.

print( 'method sort():' )
L1 = [6, -2, 5, 0, 12, 5]
print( L1 )
L1.sort()
print( L1 );
print()

# Sorting in reverse non-increasing (non-ascending) order
# can be done in both sort() and sorted() by specifying
# the reverse parameter:

print( 'reverse sorting by  sort() and sorted():' )
L1 = [6, -2, 5, 0, 12, 5]
sortedL1  = sorted( L1, reverse = True )
print( L1 ); print( sortedL1 )
L1 = [6, -2, 5, 0, 12, 5]
print( L1 )
L1.sort( reverse = True )
print( L1 );
print()




# 2. Sorting more structured items

# Example:
# List of items, each item is a list values
# Sorting methods needs a specification,
# what information/value related to an item in the array
# is a key to its position in the sorted array.
# The array is then sorted in the order of that key information.

print( 'sorting by the second component in each item :' )
L1 = [ [1,2,3],  [4,0,4,5],  [1,7],  [5,1,2],  [9,0,0] ]
print( L1 )
sortedL1  = sorted( L1, key = lambda x: x[1] )
# lambda takes  the parameter which immediately follows it
# and transforms it to the expression after the colon.
# Thus, any item x that is being sorted, is sorted according
# to its second component.
# Keyword 'key' is followed by the equality sign and then by
# specification of the property by which the items are sorted.
# The property is defined by a function, typically supplied by the programmer.
print( sortedL1 )
print()

print( 'sorting by the sum of components of each item :' )
L1 = [ [1,2,3],  [4,0,4,5],  [1,7],  [5,1,2],  [9,0,0] ]
print( L1 )
# note the change in the lambda function
sortedL1  = sorted( L1, key = lambda x: sum( x ) )
print( sortedL1 )
print( 'To verify visually:' )
print( [ (item, sum(item))  for item in sortedL1 ]  )
print()





# 3. Sorting using more criteria simultaneously
#    with the help of comparator function


# We are given a list of strings.
# The sort criteria in the descending order of importance are:
#   1. Number of characters 'a' in the string  ( more 'a's is more important )
#   2. Length of the string                    ( shorter is more important )
#   3. First character of the string           ( respect alphabet order )
#
# We have to provide a comparator -- function which takes two strings and
# returns a value indicating the relation between the strings -
# -1  first string is smaller than the second one
#  0  strings are equal
#  1  first string is bigger than the second one

def ourComparator( str1, str2 ):
    # 1. criterio`n -  Number of characters 'a' in the string
    a_count1 = str1.count( "a" )
    a_count2 = str2.count( "a" )
    if a_count1 > a_count2: return -1  # str1 is smaller = more important
    if a_count1 < a_count2: return  1  # str1 is bigger  = less important

    # now, the number of a's in both strings is the same

    # when items are equal in the 1st criterion, consider the 2nd criterion:
    if len(str1) < len(str2):  return -1  # str1 is smaller = more important
    if len(str1) > len(str2):  return 1   # str1 is bigger  = less important

    # now, the strings are equal according to both 1st and 2nd criterion:

    # consider the third criterion
    if str1[0] < str2[0]:  return -1   # str1 is smaller = more important
    if str1[0] > str2[0]:  return  1   # str1 is bigger  = less important

    # now, strings are considered equal, or better, we do not dinstinguish between them
    return 0

import functools
# In Python 3.x, the comparator has to be turned FORMALLY(!) into a key
# like in the example:
#    myComparatorKey = functools.cmp_to_key( myComparatorFunction )
#    myList.sort( key=myComparatorKey )

ourComparatorKey = functools.cmp_to_key( ourComparator )

print( 'sorting by complex criteria :' )
L1 = [ 'hello', 'zaxa', 'zaaaa', 'zaa', 'baxa', 'f' ]
print( L1 )
sortedL1  = sorted( L1, key = ourComparatorKey )
print( sortedL1 )
print()


# Note on Sorts

# Although there are relatively rare instances
# when a custom built search would be slightly
# faster than a built-in sort method,
# the built-in method is reliably fast and if used with care
# it does not affect seriously the time of the whole code execution.
# When regarding the effectivity of the sort,
# the most simple and crude rule of thumb may be the following:
# Data size < 1000
#     sort method passes through the entire data list less than 10 times.
# Data size > 1000
#     sort method passes through the entire data list more than 10 times,
#                                                 but less than 50 times.

# Example problem: Pairs of values with given sum

# In a given array, find all pairs of values
# which sum equals to a given fixed value.
# Sort the array, consider a pair P
# made of the first and the last item in the sorted array.
# Now, proceed in steps:
# In one step make the following checks
#    If the sum of P is too small, substitute the first item in P
#         by its neighbour to the right in the sorted array.
#         That neighbour value is either the same or bigger.
#    If the sum of P is too big, substitute the second item in P
#         by its neighbour to the left in the sorted array.
#         That neighbour value is either the same or smaller.
#    If the sum of P is equal to the given sum value,
#         either stop and announce success
#         or substitute both values in the pair by their respective
#         closest different values to the right and to the left.
# Stop when the first value in the pair
# is bigger or equal to the second value in the pair.


def allValuePairsWithGivenSum( arr, givenSum ):
    arrSrt = sorted( arr )
    pairs = []
    index1, index2 = 0, len(arr)-1
    while index1 < index2:
        currSum = arrSrt[index1] + arrSrt[index2]

        # the two candidates, on index1 and index2,
        # are not appropriate
        if currSum < givenSum:
           index1 += 1       # try to increase the current sum
        if currSum > givenSum:
           index2 -= 1       # try to decrease the current sum

        # now, an apprropriate pair has been found,
        # register it and move to a next candidate
        if currSum == givenSum:
            pairs.append( [arrSrt[index1], arrSrt[index2]] )

            # time to stop?
            if arrSrt[index1] == arrSrt[index2]:
                break

            # move to next candidate on the left
            currLeftValue = arrSrt[index1]
            index1 += 1
            while arrSrt[index1] == currLeftValue: index1 += 1

            # move to next candidate on the right
            currRightValue = arrSrt[index2]
            index2 -= 1
            while arrSrt[index2] == currLeftValue: index2 -= 1

        # end of while

    return   pairs

a = [ 9, 4, 11, 7, 3, 3, 4, 5, 2, 9, 1, 4, 6 ]
givenSum = 10
w = allValuePairsWithGivenSum( a, givenSum )
print(w)




'''
a = [ [6,10], [7,13], [2, 20], [9, 11] ]
b = [ [6,10], [7,13], [2, 20], [9, 11] ]
z = sorted( a, key=lambda x: x[0] )
b.sort( key=lambda x: x[1] )

print( z )
print( b )
'''
