<?php
namespace MathPHP\Tests\Statistics;

use MathPHP\Statistics\Descriptive;
use MathPHP\Exception;

class DescriptiveTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @testCase     range
     * @dataProvider dataProviderForRange
     * @param        array $numbers
     * @param        float $range
     */
    public function testRange(array $numbers, float $range)
    {
        $this->assertEquals($range, Descriptive::range($numbers), '', 0.01);
    }

    /**
     * Data provider for range test
     * @return array [ [ numbers ], range ]
     */
    public function dataProviderForRange(): array
    {
        return [
            [ [ 1, 1, 1 ], 0 ],
            [ [ 1, 1, 2 ], 1 ],
            [ [ 1, 2, 1 ], 1 ],
            [ [ 8, 4, 3 ], 5 ],
            [ [ 9, 7, 8 ], 2 ],
            [ [ 13, 18, 13, 14, 13, 16, 14, 21, 13 ], 8 ],
            [ [ 1, 2, 4, 7 ], 6 ],
            [ [ 8, 9, 10, 10, 10, 11, 11, 11, 12, 13 ], 5 ],
            [ [ 6, 7, 8, 10, 12, 14, 14, 15, 16, 20 ], 14 ],
            [ [ 9, 10, 11, 13, 15, 17, 17, 18, 19, 23 ], 14 ],
            [ [ 12, 14, 16, 20, 24, 28, 28, 30, 32, 40 ], 28 ],
        ];
    }

    /**
     * @testCase range when array is empty
     */
    public function testRangeNullWhenEmptyArray()
    {
        $this->assertNull(Descriptive::range(array()));
    }

    /**
     * @testCase     midrange
     * @dataProvider dataProviderForMidrange
     * @param        array $numbers
     * @param        float $midrange
     */
    public function testMidrange(array $numbers, float $midrange)
    {
        $this->assertEquals($midrange, Descriptive::midrange($numbers), '', 0.01);
    }

    /**
     * Data provider for midrange test
     * @return array [ [ numbers ], range ]
     */
    public function dataProviderForMidrange(): array
    {
        return [
            [ [ 1, 1, 1 ], 1 ],
            [ [ 1, 1, 2 ], 1.5 ],
            [ [ 1, 2, 1 ], 1.5 ],
            [ [ 8, 4, 3 ], 5.5 ],
            [ [ 9, 7, 8 ], 8 ],
            [ [ 13, 18, 13, 14, 13, 16, 14, 21, 13 ], 17 ],
            [ [ 1, 2, 4, 7 ], 4 ],
            [ [ 8, 9, 10, 10, 10, 11, 11, 11, 12, 13 ], 10.5 ],
            [ [ 6, 7, 8, 10, 12, 14, 14, 15, 16, 20 ], 13 ],
            [ [ 9, 10, 11, 13, 15, 17, 17, 18, 19, 23 ], 16 ],
            [ [ 12, 14, 16, 20, 24, 28, 28, 30, 32, 40 ], 26 ],
        ];
    }

    /**
     * @testCase midrange when the array is empty
     */
    public function testMidrangeNullWhenEmptyArray()
    {
        $this->assertNull(Descriptive::midrange(array()));
    }

    /**
     * @testCase     populationVariance
     * @dataProvider dataProviderForPopulationVariance
     * @param        array $numbers
     * @param        float $variance
     * @throws       \Exception
     */
    public function testPopulationVariance(array $numbers, float $variance)
    {
        $this->assertEquals($variance, Descriptive::populationVariance($numbers), '', 0.01);
    }

    /**
     * Data provider for population variance test
     * @return array [ [ numbers ], variance ]
     */
    public function dataProviderForPopulationVariance(): array
    {
        return [
            [ [ -10, 0, 10, 20, 30 ], 200 ],
            [ [ 8, 9, 10, 11, 12 ], 2 ],
            [ [ 600, 470, 170, 430, 300 ], 21704 ],
            [ [ -5, 1, 8, 7, 2], 21.84 ],
            [ [ 3, 7, 34, 25, 46, 7754, 3, 6 ], 6546331.937 ],
            [ [ 4, 6, 2, 2, 2, 2, 3, 4, 1, 3 ], 1.89 ],
            [ [ -3432, 5, 23, 9948, -74 ], 20475035.6 ],
        ];
    }

    /**
     * @testCase populationVariance when the array is empty
     * @throws   \Exception
     */
    public function testPopulationVarianceNullWhenEmptyArray()
    {
        $this->assertNull(Descriptive::populationVariance(array()));
    }

    /**
     * @testCase     sampleVariance
     * @dataProvider dataProviderForSampleVariance
     * @param        array $numbers
     * @param        float $variance
     * @throws       \Exception
     */
    public function testSampleVariance(array $numbers, float $variance)
    {
        $this->assertEquals($variance, Descriptive::sampleVariance($numbers), '', 0.01);
    }

    /**
     * Data provider for sample variance test
     * @return array [ [ numbers ], variance ]
     */
    public function dataProviderForSampleVariance(): array
    {
        return [
            [ [ -10, 0, 10, 20, 30 ], 250 ],
            [ [ 8, 9, 10, 11, 12 ], 2.5 ],
            [ [ 600, 470, 170, 430, 300 ], 27130 ],
            [ [ -5, 1, 8, 7, 2 ], 27.3 ],
            [ [ 3, 7, 34, 25, 46, 7754, 3, 6 ], 7481522.21429 ],
            [ [ 4, 6, 2, 2, 2, 2, 3, 4, 1, 3 ], 2.1 ],
            [ [ -3432, 5, 23, 9948, -74 ], 25593794.5 ],
            [ [ 3, 21, 98, 203, 17, 9 ],  6219.9 ],
            [ [ 170, 300, 430, 470, 600 ], 27130 ],
            [ [ 1550, 1700, 900, 850, 1000, 950 ], 135416.66668 ],
            [ [ 1245, 1255, 1654, 1547, 1787, 1989, 1878, 2011, 2145, 2545, 2656 ], 210804.29090909063 ],
        ];
    }

    /**
     * @testCase sampleVariance when the array is empty
     * @throws   \Exception
     */
    public function testSampleVarianceNullWhenEmptyArray()
    {
        $this->assertNull(Descriptive::sampleVariance(array()));
    }

    /**
     * @testCase sampleVariance when the array only contains one item
     * @throws   \Exception
     */
    public function testSampleVarianceZeroWhenListContainsOnlyOneItem()
    {
        $this->assertEquals(0, Descriptive::sampleVariance([5]));
    }

    /**
     * @testCase variance when the degrees of freedom is less than zero
     * @throws   \Exception
     */
    public function testVarianceExceptionDFLessThanZero()
    {
        $this->expectException(Exception\OutOfBoundsException::class);
        Descriptive::variance([1, 2, 3], -1);
    }

    /**
     * @testCase     weightedSampleVariance unbiased
     * @dataProvider dataProviderForWeightedSampleVarianceUnbiased
     * @param        array $numbers
     * @param        array $weights
     * @param        float $variance
     * @throws       \Exception
     */
    public function testWeightedSampleVarianceUnbiased(array $numbers, array $weights, float $variance)
    {
        $this->assertEquals($variance, Descriptive::weightedSampleVariance($numbers, $weights), '', 0.00001);
    }

     /**
     * Data provider for weighted sample variance test
     * @return array [ [ numbers, weights ], variance ]
     */
    public function dataProviderForWeightedSampleVarianceUnbiased(): array
    {
        return [
            [ [ -10, 0, 10, 20, 30 ], [1, 1, 1, 1, 1], 250 ],
            [ [ 8, 9, 10, 11, 12 ], [1, 1, 1, 1, 1], 2.5 ],
            [ [ 600, 470, 170, 430, 300 ], [1, 1, 1, 1, 1], 27130 ],
            [ [ -5, 1, 8, 7, 2 ], [1, 1, 1, 1, 1], 27.3 ],
            [ [ 3, 7, 34, 25, 46, 7754, 3, 6 ], [1, 1, 1, 1, 1, 1, 1, 1], 7481522.21429 ],
        ];
    }

    /**
     * @testCase     weightedSampleVariance biased
     * @dataProvider dataProviderForWeightedSampleVarianceBiased
     * @param        array $numbers
     * @param        array $weights
     * @param        float $variance
     * @throws       \Exception
     */
    public function testWeightedSampleVarianceBiased(array $numbers, array $weights, float $variance)
    {
        $biased = true;
        $this->assertEquals($variance, Descriptive::weightedSampleVariance($numbers, $weights, $biased), '', 0.1);
    }

    /**
     * Data provider for weighted sample variance test
     * Test data created with R package Weighted.Desc.Stat: w.var(x, w)
     * @return array [ [ numbers, weights ], variance ]
     */
    public function dataProviderForWeightedSampleVarianceBiased(): array
    {
        return [
            [ [ -10, 0, 10, 20, 30 ], [1, 1, 1, 1, 1], 200 ],
            [ [ 8, 9, 10, 11, 12 ], [1, 1, 1, 1, 1], 2 ],
            [ [ 8, 9, 10, 11, 12 ], [0.3, 0.3, 0.2, 0.2, 0.1], 1.702479 ],
            [ [ 600, 470, 170, 430, 300 ], [1, 1, 1, 1, 1], 21704 ],
            [ [ -5, 1, 8, 7, 2 ], [1, 1, 1, 1, 1], 21.84 ],
            [ [ 3, 7, 34, 25, 46, 7754, 3, 6 ], [1, 1, 1, 1, 1, 1, 1, 1], 6546332 ],
        ];
    }

    /**
     * @testCase weightedSampleVariance is zero when there is only one number.
     * @throws   Exception\BadDataException
     */
    public function testWeightedSampleVarianceSetOfOne()
    {
        $numbers = [4];
        $weights = [1];
        $this->assertEquals(0, Descriptive::weightedSampleVariance($numbers, $weights));
    }

    /**
     * @testCase weightedSampleVariance throws a BadDataException if the weights and numbers have different counts
     * @throws   Exception\BadDataException
     */
    public function testWeightedSampleVarianceException()
    {
        $numbers = [1, 2, 3];
        $weights = [1, 1];

        $this->expectException(Exception\BadDataException::class);
        Descriptive::weightedSampleVariance($numbers, $weights);
    }

    /**
     * @testCase     standardDeviation
     * @dataProvider dataProviderForStandardDeviationUsingPopulationVariance
     * @param        array $numbers
     * @param        float $standard_deviation
     * @throws       \Exception
     */
    public function testStandardDeviationUsingPopulationVariance(array $numbers, float $standard_deviation)
    {
        $this->assertEquals($standard_deviation, Descriptive::standardDeviation($numbers, true), '', 0.01);
    }

    /**
     * @testCase     sd
     * @dataProvider dataProviderForStandardDeviationUsingPopulationVariance
     * @param        array $numbers
     * @param        float $standard_deviation
     * @throws       \Exception
     */
    public function testSdUsingPopulationVariance(array $numbers, float $standard_deviation)
    {
        $this->assertEquals($standard_deviation, Descriptive::sd($numbers, true), '', 0.01);
    }

    /**
     * Data provider for standard deviation test
     * @return array [ [ numbers ], mean ]
     */
    public function dataProviderForStandardDeviationUsingPopulationVariance(): array
    {
        return [
            [ [ -10, 0, 10, 20, 30 ], 10 * sqrt(2) ],
            [ [ 8, 9, 10, 11, 12 ], sqrt(2) ],
            [ [ 600, 470, 170, 430, 300], 147.32 ],
            [ [ -5, 1, 8, 7, 2], 4.67 ],
            [ [ 3, 7, 34, 25, 46, 7754, 3, 6 ], 2558.580063 ],
            [ [ 4, 6, 2, 2, 2, 2, 3, 4, 1, 3 ], 1.374772708 ],
            [ [ -3432, 5, 23, 9948, -74 ], 4524.934872 ],
        ];
    }

    /**
     * @testCase     standardDeviation
     * @dataProvider dataProviderForStandardDeviationUsingSampleVariance
     * @param        array $numbers
     * @param        float $standard_deviation
     * @throws       \Exception
     */
    public function testStandardDeviationUsingSampleVariance(array $numbers, float $standard_deviation)
    {
        $this->assertEquals($standard_deviation, Descriptive::standardDeviation($numbers), '', 0.01);
    }

    /**
     * @testCase     sd
     * @dataProvider dataProviderForStandardDeviationUsingSampleVariance
     * @param        array $numbers
     * @param        float $standard_deviation
     * @throws       \Exception
     */
    public function testSDeviationUsingSampleVariance(array $numbers, float $standard_deviation)
    {
        $this->assertEquals($standard_deviation, Descriptive::sd($numbers), '', 0.01);
    }

    /**
     * Data provider for standard deviation using sample variance test
     * @return array [ [ numbers ], mean ]
     */
    public function dataProviderForStandardDeviationUsingSampleVariance(): array
    {
        return [
            [ [ 3, 21, 98, 203, 17, 9 ],  78.86634 ],
            [ [ 170, 300, 430, 470, 600 ], 164.7118696390761 ],
            [ [ 1550, 1700, 900, 850, 1000, 950 ], 367.99 ],
            [ [ 1245, 1255, 1654, 1547, 1787, 1989, 1878, 2011, 2145, 2545, 2656 ], 459.13 ],
        ];
    }

    /**
     * @testCase standardDeviation when the array is empty
     * @throws   \Exception
     */
    public function testStandardDeviationNullWhenEmptyArray()
    {
        $this->assertNull(Descriptive::standardDeviation(array()));
    }

    /**
     * @testCase sd when the array is empty
     * @throws   \Exception
     */
    public function testSDNullWhenEmptyArray()
    {
        $this->assertNull(Descriptive::sd(array()));
    }

    /**
     * @testCase     meanAbsoluteDeviation
     * @dataProvider dataProviderForMeanAbsoluteDeviation
     * @param        array $numbers
     * @param        float $mad
     */
    public function testMeanAbsoluteDeviation(array $numbers, float $mad)
    {
        $this->assertEquals($mad, Descriptive::meanAbsoluteDeviation($numbers), '', 0.01);
    }

    /**
     * Data provider for MAD (mean) test
     * @return array [ [ numbers ], mad ]
     */
    public function dataProviderForMeanAbsoluteDeviation(): array
    {
        return [
            [ [ 92, 83, 88, 94, 91, 85, 89, 90 ], 2.75 ],
            [ [ 2, 2, 3, 4, 14 ], 3.6 ],
        ];
    }

    /**
     * @testCase meanAbsoluteDeviation when the array is empty
     */
    public function testMeanAbsoluteDeviationNullWhenEmptyArray()
    {
        $this->assertNull(Descriptive::meanAbsoluteDeviation(array()));
    }

    /**
     * @testCase     medianAbsoluteDeviation
     * @dataProvider dataProviderForMedianAbsoluteDeviation
     * @param        array $numbers
     * @param        float $mad
     */
    public function testMedianAbsoluteDeviation(array $numbers, float $mad)
    {
        $this->assertEquals($mad, Descriptive::medianAbsoluteDeviation($numbers), '', 0.01);
    }

    /**
     * Data provider for MAD (median) test
     * @return array [ [ numbers ], mad ]
     */
    public function dataProviderForMedianAbsoluteDeviation(): array
    {
        return [
            [ [ 1, 1, 2, 2, 4, 6, 9 ], 1 ],
            [ [ 92, 83, 88, 94, 91, 85, 89, 90 ], 2 ],
            [ [ 2, 2, 3, 4, 14 ], 1 ],
        ];
    }

    /**
     * @testCase medianAbsoluteDeviation when array is empty
     */
    public function testMedianAbsoluteDeviationNullWhenEmptyArray()
    {
        $this->assertNull(Descriptive::medianAbsoluteDeviation(array()));
    }

    /**
     * @testCase     quartilesExclusive
     * @dataProvider dataProviderForQuartilesExclusive
     * @param        array $numbers
     * @param        array $quartiles
     */
    public function testQuartilesExclusive(array $numbers, array $quartiles)
    {
        $this->assertEquals($quartiles, Descriptive::quartilesExclusive($numbers));
    }

    /**
     * @return array [numbers, quartiles]
     */
    public function dataProviderForQuartilesExclusive(): array
    {
        return [
            [
                [ 6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49],
                [ '0%' => 6, 'Q1' => 15, 'Q2' => 40, 'Q3' => 43, '100%' => 49, 'IQR' => 28 ],
            ],
            [
                [ 7, 15, 36, 39, 40, 41 ],
                [ '0%' => 7, 'Q1' => 15, 'Q2' => 37.5, 'Q3' => 40, '100%' => 41, 'IQR' => 25 ],
            ],
            [
                [ 0, 2, 2, 4, 5, 6, 7, 7, 8, 9, 34, 34, 43, 54, 54, 76, 234 ],
                [ '0%' => 0, 'Q1' => 4.5, 'Q2' => 8, 'Q3' => 48.5, '100%' => 234, 'IQR' => 44 ],
            ]
        ];
    }

    /**
     * @testCase quartilesExclusive when the array is empty
     */
    public function testQuartilesExclusiveEmptyWhenEmptyArray()
    {
        $this->assertEmpty(Descriptive::quartilesExclusive(array()));
    }

    /**
     * @testCase     quartilesInclusive
     * @dataProvider dataProviderForQuartilesInclusive
     * @param        array $numbers
     * @param        array $quartiles
     */
    public function testQuartilesInclusive(array $numbers, array $quartiles)
    {
        $this->assertEquals($quartiles, Descriptive::quartilesInclusive($numbers));
    }

    /**
     * @return array [numbers, quartiles]
     */
    public function dataProviderForQuartilesInclusive(): array
    {
        return [
            [
                [ 6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49],
                [ '0%' => 6, 'Q1' => 25.5, 'Q2' => 40, 'Q3' => 42.5, '100%' => 49, 'IQR' => 17 ],
            ],
            [
                [ 7, 15, 36, 39, 40, 41 ],
                [ '0%' => 7, 'Q1' => 15, 'Q2' => 37.5, 'Q3' => 40, '100%' => 41, 'IQR' => 25 ],
            ],
            [
                [ 0, 2, 2, 4, 5, 6, 7, 7, 8, 9, 34, 34, 43, 54, 54, 76, 234 ],
                [ '0%' => 0, 'Q1' => 5, 'Q2' => 8, 'Q3' => 43, '100%' => 234, 'IQR' => 38 ],
            ]
        ];
    }

    /**
     * @testCase quartilesInclusive when the array is empty
     */
    public function testQuartilesInclusiveEmptyWhenEmptyArray()
    {
        $this->assertEmpty(Descriptive::quartilesInclusive(array()));
    }

    /**
     * @testCase     quartiles
     * @dataProvider dataProviderForQuartiles
     * @param        array $numbers
     * @param        string $method
     * @param        array $quartiles
     */
    public function testQuartiles(array $numbers, string $method, array $quartiles)
    {
        $this->assertEquals($quartiles, Descriptive::quartiles($numbers, $method));
    }

    /**
     * @return array [numbers, method, quartiles]
     */
    public function dataProviderForQuartiles(): array
    {
        return [
            [
                [ 6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49 ], 'Exclusive',
                [ '0%' => 6, 'Q1' => 15, 'Q2' => 40, 'Q3' => 43, '100%' => 49, 'IQR' => 28 ],
            ],
            [
                [ 7, 15, 36, 39, 40, 41 ], 'Exclusive',
                [ '0%' => 7, 'Q1' => 15, 'Q2' => 37.5, 'Q3' => 40, '100%' => 41, 'IQR' => 25 ],
            ],
            [
                [ 0, 2, 2, 4, 5, 6, 7, 7, 8, 9, 34, 34, 43, 54, 54, 76, 234 ], 'Exclusive',
                [ '0%' => 0, 'Q1' => 4.5, 'Q2' => 8, 'Q3' => 48.5, '100%' => 234, 'IQR' => 44 ],
            ],
            [
                [ 6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49 ], 'Inclusive',
                [ '0%' => 6, 'Q1' => 25.5, 'Q2' => 40, 'Q3' => 42.5, '100%' => 49, 'IQR' => 17 ],
            ],
            [
                [ 7, 15, 36, 39, 40, 41 ], 'Inclusive',
                [ '0%' => 7, 'Q1' => 15, 'Q2' => 37.5, 'Q3' => 40, '100%' => 41, 'IQR' => 25 ],
            ],
            [
                [ 0, 2, 2, 4, 5, 6, 7, 7, 8, 9, 34, 34, 43, 54, 54, 76, 234 ], 'Inclusive',
                [ '0%' => 0, 'Q1' => 5, 'Q2' => 8, 'Q3' => 43, '100%' => 234, 'IQR' => 38 ],
            ],
            [
                [ 6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49 ], 'Not_A_Real_Method_So_Default_Is_Used_Which_Is_Exclusive',
                [ '0%' => 6, 'Q1' => 15, 'Q2' => 40, 'Q3' => 43, '100%' => 49, 'IQR' => 28 ],
            ],
            [
                [ 7, 15, 36, 39, 40, 41 ], 'Not_A_Real_Method_So_Default_Is_Used_Which_Is_Exclusive',
                [ '0%' => 7, 'Q1' => 15, 'Q2' => 37.5, 'Q3' => 40, '100%' => 41, 'IQR' => 25 ],
            ],
            [
                [ 0, 2, 2, 4, 5, 6, 7, 7, 8, 9, 34, 34, 43, 54, 54, 76, 234 ], 'Not_A_Real_Method_So_Default_Is_Used_Which_Is_Exclusive',
                [ '0%' => 0, 'Q1' => 4.5, 'Q2' => 8, 'Q3' => 48.5, '100%' => 234, 'IQR' => 44 ],
            ],
        ];
    }

    /**
     * @testCase     interquartileRange
     * @dataProvider dataProviderForIQR
     * @param        array $numbers
     * @param        float $IQR
     */
    public function testInterquartileRange(array $numbers, float $IQR)
    {
        $this->assertEquals($IQR, Descriptive::interquartileRange($numbers));
    }

    /**
     * @testCase     iqr
     * @dataProvider dataProviderForIQR
     * @param        array $numbers
     * @param        float $IQR
     */
    public function testIQR(array $numbers, float $IQR)
    {
        $this->assertEquals($IQR, Descriptive::iqr($numbers));
    }

    /**
     * @return array [numbers, iqr]
     */
    public function dataProviderForIQR(): array
    {
        return [
            [ [ 6, 7, 15, 36, 39, 40, 41, 42, 43, 47, 49], 28 ],
            [ [ 7, 15, 36, 39, 40, 41 ], 25 ],
        ];
    }

    /**
     * @testCase     percentile
     * @dataProvider dataProviderForPercentile
     * @param        array $numbers
     * @param        float $percentile
     * @param        float $value
     * @throws       \Exception
     */
    public function testPercentile(array $numbers, float $percentile, float $value)
    {
        $this->assertEquals($value, Descriptive::percentile($numbers, $percentile), '', 0.0000001);
    }

    /**
     * @return array [numbres, percentile, value]
     */
    public function dataProviderForPercentile(): array
    {
        return [
            // Wikipedia
            [[15, 20, 35, 40, 50], 40, 29],
            [[1, 2, 3, 4], 75, 3.25],

            // numpy.percentile / Excel 2015 Mac
            // All int percentiles 0 - 100
            [[15, 20, 35, 40, 50], 0, 15],
            [[15, 20, 35, 40, 50], 1, 15.2],
            [[15, 20, 35, 40, 50], 2, 15.4],
            [[15, 20, 35, 40, 50], 3, 15.6],
            [[15, 20, 35, 40, 50], 4, 15.8],
            [[15, 20, 35, 40, 50], 5, 16.0],
            [[15, 20, 35, 40, 50], 6, 16.2],
            [[15, 20, 35, 40, 50], 7, 16.4],
            [[15, 20, 35, 40, 50], 8, 16.6],
            [[15, 20, 35, 40, 50], 9, 16.8],
            [[15, 20, 35, 40, 50], 10, 17.0],
            [[15, 20, 35, 40, 50], 11, 17.2],
            [[15, 20, 35, 40, 50], 12, 17.4],
            [[15, 20, 35, 40, 50], 13, 17.6],
            [[15, 20, 35, 40, 50], 14, 17.8],
            [[15, 20, 35, 40, 50], 15, 18.0],
            [[15, 20, 35, 40, 50], 16, 18.2],
            [[15, 20, 35, 40, 50], 17, 18.4],
            [[15, 20, 35, 40, 50], 18, 18.6],
            [[15, 20, 35, 40, 50], 19, 18.8],
            [[15, 20, 35, 40, 50], 20, 19.0],
            [[15, 20, 35, 40, 50], 21, 19.2],
            [[15, 20, 35, 40, 50], 22, 19.4],
            [[15, 20, 35, 40, 50], 23, 19.6],
            [[15, 20, 35, 40, 50], 24, 19.8],
            [[15, 20, 35, 40, 50], 25, 20.0],
            [[15, 20, 35, 40, 50], 26, 20.6],
            [[15, 20, 35, 40, 50], 27, 21.2],
            [[15, 20, 35, 40, 50], 28, 21.8],
            [[15, 20, 35, 40, 50], 29, 22.4],
            [[15, 20, 35, 40, 50], 30, 23.0],
            [[15, 20, 35, 40, 50], 31, 23.6],
            [[15, 20, 35, 40, 50], 32, 24.2],
            [[15, 20, 35, 40, 50], 33, 24.8],
            [[15, 20, 35, 40, 50], 34, 25.4],
            [[15, 20, 35, 40, 50], 35, 26.0],
            [[15, 20, 35, 40, 50], 36, 26.6],
            [[15, 20, 35, 40, 50], 37, 27.2],
            [[15, 20, 35, 40, 50], 38, 27.8],
            [[15, 20, 35, 40, 50], 39, 28.4],
            [[15, 20, 35, 40, 50], 40, 29.0],
            [[15, 20, 35, 40, 50], 41, 29.6],
            [[15, 20, 35, 40, 50], 42, 30.2],
            [[15, 20, 35, 40, 50], 43, 30.8],
            [[15, 20, 35, 40, 50], 44, 31.4],
            [[15, 20, 35, 40, 50], 45, 32.0],
            [[15, 20, 35, 40, 50], 46, 32.6],
            [[15, 20, 35, 40, 50], 47, 33.2],
            [[15, 20, 35, 40, 50], 48, 33.8],
            [[15, 20, 35, 40, 50], 49, 34.4],
            [[15, 20, 35, 40, 50], 50, 35.0],
            [[15, 20, 35, 40, 50], 51, 35.2],
            [[15, 20, 35, 40, 50], 52, 35.4],
            [[15, 20, 35, 40, 50], 53, 35.6],
            [[15, 20, 35, 40, 50], 54, 35.8],
            [[15, 20, 35, 40, 50], 55, 36.0],
            [[15, 20, 35, 40, 50], 56, 36.2],
            [[15, 20, 35, 40, 50], 57, 36.4],
            [[15, 20, 35, 40, 50], 58, 36.6],
            [[15, 20, 35, 40, 50], 59, 36.8],
            [[15, 20, 35, 40, 50], 60, 37.0],
            [[15, 20, 35, 40, 50], 61, 37.2],
            [[15, 20, 35, 40, 50], 62, 37.4],
            [[15, 20, 35, 40, 50], 63, 37.6],
            [[15, 20, 35, 40, 50], 64, 37.8],
            [[15, 20, 35, 40, 50], 65, 38.0],
            [[15, 20, 35, 40, 50], 66, 38.2],
            [[15, 20, 35, 40, 50], 67, 38.4],
            [[15, 20, 35, 40, 50], 68, 38.6],
            [[15, 20, 35, 40, 50], 69, 38.8],
            [[15, 20, 35, 40, 50], 70, 39.0],
            [[15, 20, 35, 40, 50], 71, 39.2],
            [[15, 20, 35, 40, 50], 72, 39.4],
            [[15, 20, 35, 40, 50], 73, 39.6],
            [[15, 20, 35, 40, 50], 74, 39.8],
            [[15, 20, 35, 40, 50], 75, 40.0],
            [[15, 20, 35, 40, 50], 76, 40.4],
            [[15, 20, 35, 40, 50], 77, 40.8],
            [[15, 20, 35, 40, 50], 78, 41.2],
            [[15, 20, 35, 40, 50], 79, 41.6],
            [[15, 20, 35, 40, 50], 80, 42.0],
            [[15, 20, 35, 40, 50], 81, 42.4],
            [[15, 20, 35, 40, 50], 82, 42.8],
            [[15, 20, 35, 40, 50], 83, 43.2],
            [[15, 20, 35, 40, 50], 84, 43.6],
            [[15, 20, 35, 40, 50], 85, 44.0],
            [[15, 20, 35, 40, 50], 86, 44.4],
            [[15, 20, 35, 40, 50], 87, 44.8],
            [[15, 20, 35, 40, 50], 88, 45.2],
            [[15, 20, 35, 40, 50], 89, 45.6],
            [[15, 20, 35, 40, 50], 90, 46.0],
            [[15, 20, 35, 40, 50], 91, 46.4],
            [[15, 20, 35, 40, 50], 92, 46.8],
            [[15, 20, 35, 40, 50], 93, 47.2],
            [[15, 20, 35, 40, 50], 94, 47.6],
            [[15, 20, 35, 40, 50], 95, 48.0],
            [[15, 20, 35, 40, 50], 96, 48.4],
            [[15, 20, 35, 40, 50], 97, 48.8],
            [[15, 20, 35, 40, 50], 98, 49.2],
            [[15, 20, 35, 40, 50], 99, 49.6],
            [[15, 20, 35, 40, 50], 100, 50.0],

            // Float percentiles
            [[15, 20, 35, 40, 50], 0.5, 15.1],
            [[15, 20, 35, 40, 50], 1.5, 15.299999999999999],
            [[15, 20, 35, 40, 50], 5.5, 16.100000000000001],
            [[15, 20, 35, 40, 50], 50.5, 35.099999999999994],
            [[15, 20, 35, 40, 50], 60.2, 37.039999999999999],
            [[15, 20, 35, 40, 50], 91.8, 46.719999999999999],
            [[15, 20, 35, 40, 50], 99.9, 49.960000000000008],

            // Edge case: one-element list
            [[5], 0, 5],
            [[5], 1, 5],
            [[5], 50.5, 5],
            [[5], 99, 5],
            [[5], 100, 5],

            // Two-element list
            [[2, 3], 0, 2],
            [[2, 3], 1, 2.01],
            [[2, 3], 50.5, 2.505],
            [[2, 3], 99, 2.9899999999999998],
            [[2, 3], 100, 3],

            // Big list
            [[1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,9,9,9,9,9,9,9,8,9,8,9,8,9,8,7,6,5,4,3,2,1,2,3,4,3,4,3,4,5,6,7,8,7,8,7,8,9,0,0,9,0,9,8,7,6,5,4,3,2,1], 45.3, 5],

            // More random test cases
            [[3, 6, 7, 8, 8, 10, 13, 15, 16, 20], 0, 3.0 ],
            [[3, 6, 7, 8, 8, 10, 13, 15, 16, 20], 25, 7.25 ],
            [[3, 6, 7, 8, 8, 10, 13, 15, 16, 20], 50, 9.0 ],
            [[3, 6, 7, 8, 8, 10, 13, 15, 16, 20], 75, 14.5 ],
            [[3, 6, 7, 8, 8, 10, 13, 15, 16, 20], 100, 20.0 ],
            [[3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0, 3.0 ],
            [[3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 25, 7.5 ],
            [[3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 50, 9.0 ],
            [[3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 75, 14.0 ],
            [[3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 100, 20.0 ],
        ];
    }

    /**
     * @testCase percentile throws an Exception\BadDataException if numbers is empty
     * @throws   \Exception
     */
    public function testPercentileEmptyList()
    {
        $this->expectException(Exception\BadDataException::class);
        Descriptive::percentile([], 5);
    }

    /**
     * @testCase percentile throws an Exception\OutOfBoundsException if P is < 0
     * @throws   \Exception
     */
    public function testPercentileOutOfLowerBoundsP()
    {
        $this->expectException(Exception\OutOfBoundsException::class);
        Descriptive::percentile([1, 2, 3], -4);
    }

    /**
     * @testCase percentile throws an Exception\OutOfBoundsException if P is > 100
     * @throws   \Exception
     */
    public function testPercentileOutOfUpperBoundsP()
    {
        $this->expectException(Exception\OutOfBoundsException::class);
        Descriptive::percentile([1, 2, 3], 101);
    }

    /**
     * @testCase     midhinge
     * @dataProvider dataProviderForMidhinge
     * @param        array $numbers
     * @param        float $midhinge
     */
    public function testMidhinge(array $numbers, float $midhinge)
    {
        $this->assertEquals($midhinge, Descriptive::midhinge($numbers), '', 0.01);
    }

    /**
     * @return array [numbers, midhinge]
     */
    public function dataProviderForMidhinge(): array
    {
        return [
            [ [1, 2, 3, 4, 5, 6], 3.5 ],
            [ [5, 5, 7, 8, 8, 11, 12, 12, 14, 15, 16, 19, 21, 22, 22, 26, 26, 26, 28, 29, 29, 32, 33, 34, 34, 34, 34, 35, 35, 37, 38, 38], 23.5],
            [ [36, 34, 21, 10, 20, 24, 31, 30, 30, 30, 30, 24, 30, 24, 39, 6, 32, 33, 33, 25, 26, 35, 8, 5, 30, 40, 9, 32, 25, 40, 24, 38], 28.5],
            [ [8, 10, 11, 12, 12, 13, 17, 18, 19, 19, 21, 23, 24, 24, 25, 25, 27, 27, 28, 28, 29, 29, 30, 30, 32, 33, 34, 35, 36, 37, 37, 40], 24.75 ],
        ];
    }

    /**
     * @testCase     coefficientOfVariation
     * @dataProvider dataProviderForCoefficientOfVariation
     * @param        array $numbers
     * @param        float $cv
     * @throws       \Exception
     */
    public function testsCoefficientOfVariation(array $numbers, float $cv)
    {
        $this->assertEquals($cv, Descriptive::coefficientOfVariation($numbers), '', 0.0001);
    }

    /**
     * @return array [numbers, cv]
     */
    public function dataProviderForCoefficientOfVariation(): array
    {
        return [
            [ [1, 2, 3, 4, 5, 6 ,7, 8], 0.54433 ],
            [ [4, 7, 43, 12, 23, 76, 45, 3, 62, 23, 34, 44, 41], 0.70673 ],
            [ [3, 3, 3, 6, 6, 5, 9], 0.44721 ],
            [ [100, 100, 100], 0 ],
            [ [0, 10, 20, 30, 40], 0.7905 ],
            [ [32, 50, 68, 86, 104], 0.41852941176471 ],
        ];
    }

    /**
     * @testCase describe - population
     * @throws   \Exception
     */
    public function testDescribePopulation()
    {
        $stats = Descriptive::describe([ 13, 18, 13, 14, 13, 16, 14, 21, 13 ], true);
        $this->assertTrue(is_array($stats));
        $this->assertArrayHasKey('n', $stats);
        $this->assertArrayHasKey('min', $stats);
        $this->assertArrayHasKey('max', $stats);
        $this->assertArrayHasKey('mean', $stats);
        $this->assertArrayHasKey('median', $stats);
        $this->assertArrayHasKey('mode', $stats);
        $this->assertArrayHasKey('range', $stats);
        $this->assertArrayHasKey('midrange', $stats);
        $this->assertArrayHasKey('variance', $stats);
        $this->assertArrayHasKey('sd', $stats);
        $this->assertArrayHasKey('cv', $stats);
        $this->assertArrayHasKey('mean_mad', $stats);
        $this->assertArrayHasKey('median_mad', $stats);
        $this->assertArrayHasKey('quartiles', $stats);
        $this->assertArrayHasKey('midhinge', $stats);
        $this->assertArrayHasKey('skewness', $stats);
        $this->assertArrayHasKey('ses', $stats);
        $this->assertArrayHasKey('kurtosis', $stats);
        $this->assertArrayHasKey('sek', $stats);
        $this->assertArrayHasKey('sem', $stats);
        $this->assertArrayHasKey('ci_95', $stats);
        $this->assertArrayHasKey('ci_99', $stats);
        $this->assertTrue(is_int($stats['n']));
        $this->assertTrue(is_numeric($stats['min']));
        $this->assertTrue(is_numeric($stats['max']));
        $this->assertTrue(is_numeric($stats['mean']));
        $this->assertTrue(is_numeric($stats['median']));
        $this->assertTrue(is_array($stats['mode']));
        $this->assertTrue(is_numeric($stats['range']));
        $this->assertTrue(is_numeric($stats['midrange']));
        $this->assertTrue(is_numeric($stats['variance']));
        $this->assertTrue(is_numeric($stats['sd']));
        $this->assertTrue(is_numeric($stats['cv']));
        $this->assertTrue(is_numeric($stats['mean_mad']));
        $this->assertTrue(is_numeric($stats['median_mad']));
        $this->assertTrue(is_array($stats['quartiles']));
        $this->assertTrue(is_numeric($stats['midhinge']));
        $this->assertTrue(is_numeric($stats['skewness']));
        $this->assertTrue(is_numeric($stats['ses']));
        $this->assertTrue(is_numeric($stats['kurtosis']));
        $this->assertTrue(is_numeric($stats['sek']));
        $this->assertTrue(is_numeric($stats['sem']));
        $this->assertTrue(is_array($stats['ci_95']));
        $this->assertTrue(is_array($stats['ci_99']));
    }

    /**
     * @testCase describe - sample
     * @throws   \Exception
     */
    public function testDescribeSample()
    {
        $stats = Descriptive::describe([ 13, 18, 13, 14, 13, 16, 14, 21, 13 ], false);
        $this->assertTrue(is_array($stats));
        $this->assertArrayHasKey('n', $stats);
        $this->assertArrayHasKey('min', $stats);
        $this->assertArrayHasKey('max', $stats);
        $this->assertArrayHasKey('mean', $stats);
        $this->assertArrayHasKey('median', $stats);
        $this->assertArrayHasKey('mode', $stats);
        $this->assertArrayHasKey('range', $stats);
        $this->assertArrayHasKey('midrange', $stats);
        $this->assertArrayHasKey('variance', $stats);
        $this->assertArrayHasKey('sd', $stats);
        $this->assertArrayHasKey('cv', $stats);
        $this->assertArrayHasKey('quartiles', $stats);
        $this->assertArrayHasKey('midhinge', $stats);
        $this->assertArrayHasKey('skewness', $stats);
        $this->assertArrayHasKey('ses', $stats);
        $this->assertArrayHasKey('kurtosis', $stats);
        $this->assertArrayHasKey('sek', $stats);
        $this->assertArrayHasKey('sem', $stats);
        $this->assertArrayHasKey('ci_95', $stats);
        $this->assertArrayHasKey('ci_99', $stats);
        $this->assertTrue(is_int($stats['n']));
        $this->assertTrue(is_numeric($stats['min']));
        $this->assertTrue(is_numeric($stats['max']));
        $this->assertTrue(is_numeric($stats['mean']));
        $this->assertTrue(is_numeric($stats['median']));
        $this->assertTrue(is_array($stats['mode']));
        $this->assertTrue(is_numeric($stats['range']));
        $this->assertTrue(is_numeric($stats['midrange']));
        $this->assertTrue(is_numeric($stats['variance']));
        $this->assertTrue(is_numeric($stats['sd']));
        $this->assertTrue(is_numeric($stats['cv']));
        $this->assertTrue(is_array($stats['quartiles']));
        $this->assertTrue(is_numeric($stats['midhinge']));
        $this->assertTrue(is_numeric($stats['skewness']));
        $this->assertTrue(is_numeric($stats['ses']));
        $this->assertTrue(is_numeric($stats['kurtosis']));
        $this->assertTrue(is_numeric($stats['sek']));
        $this->assertTrue(is_numeric($stats['sem']));
        $this->assertTrue(is_array($stats['ci_95']));
        $this->assertTrue(is_array($stats['ci_99']));
    }

    /**
     * @testCase     describe will return null ses for values of n < 3
     * @dataProvider dataProviderForDescribeNullSes
     * @param        array $numbers
     * @throws       \Exception
     */
    public function testDescribeSesNullForSmallN(array $numbers)
    {
        $stats = Descriptive::describe($numbers);
        $this->assertNull($stats['ses']);
    }

    /**
     * @return array [numbers]
     */
    public function dataProviderForDescribeNullSes(): array
    {
        return [
            [[-1]],
            [[0]],
            [[1]],
            [[2]],
            [[3]],
            [[4]],
            [[5]],
            [[10]],
            [[100]],
            [[999999]],
            [[-1, -1]],
            [[-1, 0]],
            [[0, -1]],
            [[0, 0]],
            [[0, 1]],
            [[1, 0]],
            [[1, 1]],
            [[1, 2]],
            [[5, 5]],
            [[10, 10]],
            [[9293, 85732]],
        ];
    }

    /**
     * @testCase     describe will return null sek for values of n < 4
     * @dataProvider dataProviderForDescribeNullSek
     * @param        array $numbers
     * @throws       \Exception
     */
    public function testDescribeSekNullForSmallN(array $numbers)
    {
        $stats = Descriptive::describe($numbers);
        $this->assertNull($stats['sek']);
    }

    /**
     * @return array [numbers]
     */
    public function dataProviderForDescribeNullSek(): array
    {
        return [
            [[-1]],
            [[0]],
            [[1]],
            [[2]],
            [[3]],
            [[4]],
            [[5]],
            [[10]],
            [[100]],
            [[999999]],
            [[-1, -1]],
            [[-1, 0]],
            [[0, -1]],
            [[0, 0]],
            [[0, 1]],
            [[1, 0]],
            [[1, 1]],
            [[1, 2]],
            [[5, 5]],
            [[10, 10]],
            [[9293, 85732]],
            [[-1, -1, -1]],
            [[-1, 0, 0]],
            [[-1, 0, -1]],
            [[0, -1, 0]],
            [[0, -1, -1]],
            [[0, 0, -1]],
            [[-1, 0, -1]],
            [[0, 0, 0]],
            [[0, 1], 0],
            [[1, 0], 0],
            [[1, 1], 1],
            [[1, 2, 3]],
            [[5, 5], 5],
            [[10, 10, 10]],
            [[9293, 85732, 44837475]],
        ];
    }

    /**
     * @testCase     fiveNumberSummary
     * @dataProvider dataProviderForFiveNumberSummary
     * @param        array $numbers
     * @param        array $summary
     */
    public function testFiveNumberSummary(array $numbers, array $summary)
    {
        $this->assertEquals($summary, Descriptive::fiveNumberSummary($numbers), '', 0.0001);
    }

    public function dataProviderForFiveNumberSummary()
    {
        return [
            [
                [0, 0, 1, 2, 63, 61, 27, 13],
                ['min' => 0, 'Q1' => 0.5, 'median' => 7.5, 'Q3' => 44.0, 'max' => 63],
            ],
        ];
    }
}
