# combinatoricsExt.test --
#     Tests for the math::combinatorics package (the extended set of procedures)
#
source [file join \
        [file dirname [file dirname [file join [pwd] [info script]]]] \
        devtools testutilities.tcl]

testsNeedTcl     8.6
testsNeedTcltest 1.0

support {
    useLocal math.tcl math
}
testing {
    useLocal combinatoricsExt.tcl math::combinatorics
}


test counts-1.0 {Calculation of the number of permutations} -body {
    set number {}
    foreach n {1 2 5 10 20 30} {
        lappend number [::math::combinatorics::permutations $n]
    }

    set number
} -result {1 2 120 3628800 2432902008176640000 265252859812191058636308480000000}

test counts-1.1 {Permutations for zero or negative count} -body {
    set number {}
    foreach n {-1 0} {
        lappend number [::math::combinatorics::permutations $n]
    }

    set number
} -result {1 1}

test counts-2.0 {Calculation of the number of variations} -body {
    set number {}
    foreach n {1 2 5 10} {
        foreach k {1 2 5 6 7 8 9} {
            if { $k <= $n } {
                lappend number [::math::combinatorics::variations $n $k]
            }
        }
    }

    set number
} -result {1 2 2 5 20 120 10 90 30240 151200 604800 1814400 3628800}

test counts-2.1 {Variations for zero or negative counts} -body {
    set number {}
    foreach n {-1 0 1 2 5} {
        foreach k {-1 0 1 2 6} {
            lappend number [::math::combinatorics::variations $n $k]
        }
    }

    set number
} -result {0 0 0 0 0 0 1 0 0 0 0 1 1 0 0 0 1 2 2 0 0 1 5 20 0}

test counts-3.0 {Calculation of the number of combinations} -body {
    set number {}
    foreach n {1 2 5 10} {
        foreach k {1 2 5 6 7 8 9} {
            if { $k <= $n } {
                lappend number [::math::combinatorics::combinations $n $k]
            }
        }
    }

    set number
} -result {1 2 1 5 10 1 10 45 252 210 120 45 10}

test counts-3.1 {Combinations for zero or negative counts} -body {
    set number {}
    foreach n {-1 0 1 2 5} {
        foreach k {-1 0 1 2 6} {
            lappend number [::math::combinatorics::combinations $n $k]
        }
    }

    set number
} -result {0 0 0 0 0 0 1 0 0 0 0 1 1 0 0 0 1 2 1 0 0 1 5 10 0}

test counts-4.0 {Calculation of the number of derangements} -body {
    set number {}
    foreach n {1 2 3 4 5 6 7} {
        lappend number [::math::combinatorics::derangements $n]
    }

    set number
} -result {0 1 2 9 44 265 1854}

test counts-4.1 {Derangements for zero or negative counts} -body {
    set number {}
    foreach n {-1 0} {
        lappend number [::math::combinatorics::derangements $n]
    }

    set number
} -result {0 0}

#
# The reference numbers come from https://mathworld.wolfram.com
#
test numbers-1.0 {First few Catalan numbers} -body {
    set number {}
    foreach n {1 2 3 4 5 6 7 8 9 10} {
        lappend number [::math::combinatorics::catalan $n]
    }

    set number
} -result {1 2 5 14 42 132 429 1430 4862 16796}

test numbers-2.0 {Stirling numbers of the first kind} -body {
    set number {}
    foreach n {1 2 3 4 5} {
        foreach k {1 2 3 4 5} {
            if { $k <= $n } {
                lappend number [::math::combinatorics::firstStirling $n $k]
            }
        }
    }

    set number
} -result {1 -1 1 2 -3 1 -6 11 -6 1 24 -50 35 -10 1}

test numbers-2.1 {Stirling numbers of the second kind} -body {
    set number {}
    foreach n {1 2 3 4 5 6} {
        foreach k {1 2 3 4 5 6} {
            if { $k <= $n } {
                lappend number [::math::combinatorics::secondStirling $n $k]
            }
        }
    }

    set number
} -result {1 1 1 1 3 1 1 7 6 1 1 15 25 10 1 1 31 90 65 15 1}

#
# Generate collection of permutations, etc.
#
test lists-1.0 {Small number of permutations} -body {
    set collection [::math::combinatorics::list-permutations 4]
} -result {{3 2 1 0} {2 3 1 0} {2 1 3 0} {2 1 0 3} {3 1 2 0} {1 3 2 0} {1 2 3 0} {1 2 0 3} {3 1 0 2} {1 3 0 2} {1 0 3 2} {1 0 2 3} {3 2 0 1} {2 3 0 1} {2 0 3 1} {2 0 1 3} {3 0 2 1} {0 3 2 1} {0 2 3 1} {0 2 1 3} {3 0 1 2} {0 3 1 2} {0 1 3 2} {0 1 2 3}}

test lists-1.1 {Check properties of the collection of permutations} -body {
    set collection [::math::combinatorics::list-permutations 5]

    # Number of elements should be 5! = 120
    set correct_number [expr {[llength $collection] == 120}]

    # All elements should have 5 entries
    set five_entries 1
    foreach c $collection {
        if { [llength $c] != 5 } {
            set five_entries 0
            break
        }
    }

    # All entries in the elements should be unique
    set unique_entries 1
    foreach c $collection {
        if { [llength [lsort -unique $c]] != 5 } {
            set unique_entries 0
            break
        }
    }

    # All elements of the collection should be unique
    set unique_elements [expr {[llength [lsort -unique $collection]] == 120}]

    # Report our findings
    set result [list $correct_number $five_entries $unique_entries $unique_elements]
} -result {1 1 1 1}

test lists-2.0 {Small number of variations} -body {
    set collection [::math::combinatorics::list-variations 4 2]
} -result {{0 1} {1 0} {0 2} {2 0} {0 3} {3 0} {1 2} {2 1} {1 3} {3 1} {2 3} {3 2}}

test lists-2.1 {Check properties of the collection of variations} -body {
    set collection [::math::combinatorics::list-variations 5 2]

    # Number of elements should be 20
    set correct_number [expr {[llength $collection] == 20}]

    # All elements should have 2 entries
    set two_entries 1
    foreach c $collection {
        if { [llength $c] != 2 } {
            set two_entries 0
            break
        }
    }

    # All entries in the elements should be unique
    set unique_entries 1
    foreach c $collection {
        if { [llength [lsort -unique $c]] != 2 } {
            set unique_entries 0
            break
        }
    }

    # All elements of the collection should be unique
    set unique_elements [expr {[llength [lsort -unique $collection]] == 20}]

    # Report our findings
    set result [list $correct_number $two_entries $unique_entries $unique_elements]
} -result {1 1 1 1}

test lists-3.0 {Small number of combinations} -body {
    set collection [::math::combinatorics::list-combinations 4 2]
} -result {{0 1} {0 2} {0 3} {1 2} {1 3} {2 3}}

test lists-3.1 {Check properties of the collection of combinations} -body {
    set collection [::math::combinatorics::list-combinations 5 2]

    set correct_number [expr {[llength $collection] == 10}]

    # All elements should have 2 entries
    set two_entries 1
    foreach c $collection {
        if { [llength $c] != 2 } {
            set two_entries 0
            break
        }
    }

    # All entries in the elements should be unique
    set unique_entries 1
    foreach c $collection {
        if { [llength [lsort -unique $c]] != 2 } {
            set unique_entries 0
            break
        }
    }

    # All elements of the collection should be unique
    set unique_elements [expr {[llength [lsort -unique $collection]] == 10}]

    # Report our findings
    set result [list $correct_number $two_entries $unique_entries $unique_elements]
} -result {1 1 1 1}

test lists-4.0 {Small number of derangements} -body {
    set collection [::math::combinatorics::list-derangements 4]
} -result {{3 2 1 0} {2 3 1 0} {1 2 3 0} {1 3 0 2} {1 0 3 2} {3 2 0 1} {2 3 0 1} {2 0 3 1} {3 0 1 2}}

test lists-4.1 {Check properties of the collection of derangements} -body {
    set collection [::math::combinatorics::list-derangements 5]

    # Number of elements should be 44
    set correct_number [expr {[llength $collection] == 44}]

    # All elements should have 5 entries
    set five_entries 1
    foreach c $collection {
        if { [llength $c] != 5 } {
            set five_entries 0
            break
        }
    }

    # All entries in the elements should be unique
    set unique_entries 1
    foreach c $collection {
        if { [llength [lsort -unique $c]] != 5 } {
            set unique_entries 0
            break
        }
    }

    # All elements of the collection should be unique
    set unique_elements [expr {[llength [lsort -unique $collection]] == 44}]

    # All entries should be in a different place than its numeric value - {0 2 1} is not a valid derangement
    set correct_derangement 1
    foreach c $collection {
        foreach i {0 1 2 3 4} v $c {
            if { $i == $v } {
                set correct_derangement 0
                break
            }
        }
    }

    # Report our findings
    set result [list $correct_number $five_entries $unique_entries $unique_elements $correct_derangement]
} -result {1 1 1 1 1}

test lists-5.0 {Return the power set of a small set} -body {
    set powerset [::math::combinatorics::list-powerset 4]
} -result {{} 0 1 2 3 {0 1} {0 2} {0 3} {1 2} {1 3} {2 3} {0 1 2} {0 1 3} {0 2 3} {1 2 3} {0 1 2 3}}

test lists-5.1 {Check properties of the power set} -body {
    set collection [::math::combinatorics::list-powerset 5]

    # Number of elements should be 32
    set correct_number [expr {[llength $collection] == 32}]

    # All elements should have 5 or less entries
    set max_five_entries 1
    foreach c $collection {
        if { [llength $c] > 5 } {
            set max_five_entries 0
            break
        }
    }

    # All entries in the elements should be unique
    set unique_entries 1
    foreach c $collection {
        if { [llength [lsort -unique $c]] != [llength $c] } {
            set unique_entries 0
            break
        }
    }

    # All elements of the collection should be unique
    set unique_elements [expr {[llength [lsort -unique $collection]] == 32}]

    # Report our findings
    set result [list $correct_number $max_five_entries $unique_entries $unique_elements]
} -result {1 1 1 1}

#
# Permutation and combination objects
#
test objects-1.0 {List the permutations one by one} -body {
    set n   0
    set obj [::math::combinatorics::permutationObj new 4]

    set permutations {}

    while {1} {
        set permutation [$obj next]

        if { $permutation eq {} } {
            break
        }

        # Extra check
        if { [llength $permutation] ne 4 } {
            break
        }
        lappend permutations $permutation
    }

    return [llength [lsort -unique $permutations]]
} -result 24

test objects-1.1 {Permute a given set of elements} -body {
    set n   0
    set obj [::math::combinatorics::permutationObj new 4]

    $obj setElements {A B C D}

    set permutations {}

    while {1} {
        set permutation [$obj nextElements]

        if { $permutation eq {} } {
            break
        }

        set okay 1
        foreach elem {A B C D} {
            if { $elem ni $permutation } {
                set okay 0
                break
            }
        }

        # Extra check
        if { [llength $permutation] != 4 } {
            break
        }

        if { $okay } {
            lappend permutations $permutation
        }
    }

    return [llength [lsort -unique $permutations]]
} -result 24

test objects-2.0 {List the combinations one by one} -body {
    set n   0
    set obj [::math::combinatorics::combinationObj new 5 2]

    set combinations {}

    while {1} {
        set combination [$obj next]

        if { $combination eq {} } {
            break
        }

        # Extra check
        if { [llength $combination] != 2 } {
            break
        }
        lappend combinations $combination
    }

    return [llength [lsort -unique $combinations]]
} -result 10

test objects-2.1 {List combinations of a given set of elements} -body {
    set n   0
    set obj [::math::combinatorics::combinationObj new 5 2]

    set combinations {}

    $obj setElements {A B C D E}

    while {1} {
        set combination [$obj nextElements]

        if { $combination eq {} } {
            break
        }

        set okay 1
        foreach elem $combination {
            if { $elem ni {A B C D E} } {
                set okay 0
                break
            }
        }

        if { $okay } {
            lappend combinations $combination
        }
    }

    return [llength [lsort -unique $combinations]]
} -result 10

