public void CacheHitRatioCase(int n, double cacheSizePercentage, double alpha, double expectedHitRate, int maxTrials)
        {
            var zipf          = new ZipfDistribution(n, alpha, 10, 0.0001);
            var capacity      = (int)(n * cacheSizePercentage / 100);
            var cache         = new PseudoLRUCache <string>(capacity);
            var allItems      = new PseudoLRUCache <string> .CacheItem[n + 1];
            var rankHistogram = new int[n + 1];

            for (var trial = 0; trial < maxTrials; trial++)
            {
                var rank = zipf.NextRandomRank();
                if (allItems[rank] == null)
                {
                    allItems[rank] = cache.Add(rank.ToString());
                }
                else
                {
                    allItems[rank].GetOrCreate(() => rank.ToString());
                }
                rankHistogram[rank]++;
            }
            Console.WriteLine($"The Hit Ratio is {cache.HitRatio}; hoped for {expectedHitRate}");
            var histogram = "Rank Histogram for first 100 items\n";
            var cumePct   = 0.0;

            for (var rank = 1; rank <= 100; rank++)
            {
                var pct = 100.0 * rankHistogram[rank] / (double)maxTrials;
                cumePct   += pct;
                histogram += $"{rank} chosen {rankHistogram[rank]} times ({pct}%, {cumePct}% cume)\n";
            }
            Console.WriteLine(histogram);
            Assert.GreaterOrEqual(cache.HitRatio, expectedHitRate, $"Cache Hit Ratio is lower than expected: {cache.HitRatio} < {expectedHitRate}");
        }
        public void ZipfInterpolatedInverseRanksMatch()
        {
            var n       = 10000;
            var k       = 100;
            var alpha   = 1.0;
            var epsilon = 0.0002; // Uses Interpolation
            var zipf    = new ZipfDistribution(n, alpha, k, epsilon);
            var lowestRankWithDifference = new int[1000];
            var success     = true;
            var detailedLog = "";
            var countWithDifferenceMoreThanTwo = 0;
            var prevDifference = 0;

            for (var rank = 1; rank <= n; rank++)
            {
                var cdf            = zipf.CDF(rank);
                var actualRank     = zipf.Rank(cdf);
                var trueDifference = rank - actualRank;
                var difference     = Abs(trueDifference);
                if (lowestRankWithDifference[difference] == 0)
                {
                    lowestRankWithDifference[difference] = rank;
                }
                success = success && difference <= 2;

                if (difference > 0 && zipf.InterpolationCDFRanks.Contains(rank))
                {
                    Console.WriteLine($"Difference = {difference} for interpolation point at rank = {rank}");
                }
                if (trueDifference != prevDifference)
                {
                    detailedLog += $"{rank}. {trueDifference}\n";
                }
                if (difference > 2)
                {
                    countWithDifferenceMoreThanTwo++;
                }

                prevDifference = trueDifference;
            }
            Console.WriteLine(zipf.ToString());
            Console.WriteLine($"Count with Difference > 2: {countWithDifferenceMoreThanTwo}");
            for (var i = 1; i < lowestRankWithDifference.Length; i++)
            {
                if (lowestRankWithDifference[i] > 0)
                {
                    Console.WriteLine($"Difference = {i}, Rank = {lowestRankWithDifference[i]}");
                }
            }
            Console.WriteLine(detailedLog);
            Assert.IsTrue(success, $"Interpolation failed for rank {lowestRankWithDifference[3]}.");
        }
        public void ZipfUninterpolatedInverseRanksMatchWhenAlphaIsOne()
        {
            var n       = 1000;
            var alpha   = 1.0;
            var epsilon = 0; // Uses No interpolation
            var zipf    = new ZipfDistribution(n, alpha, 5, epsilon);

            for (var rank = 1; rank <= n; rank++)
            {
                var cdf        = zipf.CDF(rank);
                var actualRank = zipf.Rank(cdf);
                Assert.IsTrue(Abs(rank - actualRank) <= 1, $"Interpolation failed for rank {rank}. CDF = {cdf}. Inverse Rank {actualRank}");
            }
        }
        public void ZipfPDFValuesDecrease()
        {
            var n       = 1000;
            var alpha   = .01;
            var zipf    = new ZipfDistribution(n, alpha, 5, 0.01);
            var prevPdf = zipf.PDF(1);

            Assert.IsTrue(prevPdf < 1 && prevPdf > 0, $"PDF(1) is out of range: {prevPdf}");
            for (var rank = 2; rank <= n; rank++)
            {
                var pdf = zipf.PDF(rank);
                Assert.Less(pdf, prevPdf, $"PDF({rank}) is not less than PDF({rank-1}): {pdf}");
                prevPdf = pdf;
            }
        }
        public void ZipfApproximateCDFIsCloseToCDF()
        {
            var n       = 10000;
            var alpha   = 0.95;
            var epsilon = 0.01;
            var zipf    = new ZipfDistribution(n, alpha, 10, epsilon, ZipfDistribution.InterpolationMethod.Hyperbolic);

            Console.WriteLine(zipf.ToString());
            for (var rank = 1; rank <= n; rank++)
            {
                var expectedCdf = zipf.CDF(rank);
                var actualCdf   = zipf.ApproximateCDF(rank);
                Assert.IsFalse(RelativeErrorExceedsTolerance(expectedCdf, actualCdf, epsilon), $"Approximate CDF has unacceptable error for rank {rank} with {zipf.InterpolationSize} control points: Expected {expectedCdf} vs actual {actualCdf}");
            }
        }
        public void ZipfInterpolationSizeIncreasesWithAccuracy()
        {
            var n             = 10000;
            var alpha         = 0.95;
            var epsilons      = new[] { 0.05, 0.04, 0.03, 0.02, 0.01, 0.005, 0.001, 0.0005, 0.0001, 0.00005 };
            var previousCount = 1;

            foreach (var epsilon in epsilons)
            {
                var zipf = new ZipfDistribution(n, alpha, 10, epsilon, ZipfDistribution.InterpolationMethod.Hyperbolic);
                Console.WriteLine(zipf.ToString());
                var currentCount = zipf.InterpolationSize;
                Assert.GreaterOrEqual(currentCount, previousCount, $"Number of interpolation points decreased unexpectedly for epsilon = {epsilon}");
                previousCount = currentCount;
            }
        }
        public void ZipfCDFValuesIncreaseToOne()
        {
            var n       = 1000;
            var alpha   = 0.9;
            var zipf    = new ZipfDistribution(n, alpha, 5, 0.01);
            var prevCdf = zipf.CDF(1);

            Assert.IsTrue(prevCdf < 1 && prevCdf > 0, $"CDF(1) is out of range: {prevCdf}");
            for (var rank = 2; rank <= n; rank++)
            {
                var cdf = zipf.CDF(rank);
                Assert.Greater(cdf, prevCdf, $"CDF({rank}) is not greater than CDF({rank - 1}): {cdf}");
                prevCdf = cdf;
            }
            var actualCdf = zipf.CDF(n);

            Assert.AreEqual(1.0, actualCdf, $"CDF(N) should equal one, but is instead {actualCdf}");
        }