Skip to content

Commit 3f72dbf

Browse files
leecampbell-codeagentLeeCampbellclaude
authored
feat(#79): GetPercentileAtOrBelowValue (#135)
* plan(#79): initial brief from issue * plan(#79): review brief * plan(#79): create task breakdown * feat(#79): implement tasks * feat(#79): complete implementation * fix(#79): Address PR review feedback - Add guard for negative input values returning 0.0 - Add test for GetPercentileAtOrBelowValue(0) with positive recorded values - Add test for negative input values - Fix missing period in XML doc param tag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Lee Campbell <lee.ryan.campbell@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8b04f33 commit 3f72dbf

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

HdrHistogram.UnitTests/HistogramTestBase.cs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,146 @@ public void Setting_invalid_value_to_Tag_throws(string invalidTagValue)
361361
Assert.Throws<ArgumentException>(() => histogram.Tag = invalidTagValue);
362362
}
363363

364+
[Fact]
365+
public void GetPercentileAtOrBelowValue_EmptyHistogram_ReturnsZero()
366+
{
367+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
368+
histogram.GetPercentileAtOrBelowValue(TestValueLevel).Should().Be(0.0);
369+
}
370+
371+
[Fact]
372+
public void GetPercentileAtOrBelowValue_ValueAtOrAboveHighestTrackable_Returns100()
373+
{
374+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
375+
histogram.RecordValue(TestValueLevel);
376+
histogram.GetPercentileAtOrBelowValue(DefaultHighestTrackableValue).Should().Be(100.0);
377+
histogram.GetPercentileAtOrBelowValue(long.MaxValue / 2).Should().Be(100.0);
378+
}
379+
380+
[Fact]
381+
public void GetPercentileAtOrBelowValue_KnownValueSet_ReturnsExpectedPercentile()
382+
{
383+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
384+
for (long i = 1; i <= 100; i++)
385+
{
386+
histogram.RecordValue(i);
387+
}
388+
histogram.GetPercentileAtOrBelowValue(50).Should().BeApproximately(50.0, 0.1);
389+
histogram.GetPercentileAtOrBelowValue(100).Should().BeApproximately(100.0, 0.1);
390+
histogram.GetPercentileAtOrBelowValue(1).Should().BeApproximately(1.0, 0.1);
391+
}
392+
393+
[Fact]
394+
public void GetPercentileAtOrBelowValue_ResultIsAlwaysInRange()
395+
{
396+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
397+
for (long i = 1; i <= 10; i++)
398+
{
399+
histogram.RecordValue(i * 100);
400+
}
401+
histogram.GetPercentileAtOrBelowValue(0).Should().Be(0.0);
402+
foreach (var queryValue in new long[] { 0, 100, 500, DefaultHighestTrackableValue })
403+
{
404+
histogram.GetPercentileAtOrBelowValue(queryValue).Should().BeInRange(0.0, 100.0);
405+
}
406+
}
407+
408+
[Fact]
409+
public void GetPercentileAtOrBelowValue_RoundTrip_ConsistentWithGetValueAtPercentile()
410+
{
411+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
412+
foreach (var v in new long[] { 1, 10, 100, 1000 })
413+
{
414+
histogram.RecordValue(v);
415+
}
416+
foreach (var v in new long[] { 1, 10, 100, 1000 })
417+
{
418+
var p = histogram.GetPercentileAtOrBelowValue(v);
419+
var valueAtP = histogram.GetValueAtPercentile(p);
420+
var expected = histogram.HighestEquivalentValue(histogram.LowestEquivalentValue(v));
421+
valueAtP.Should().Be(expected);
422+
}
423+
}
424+
425+
[Fact]
426+
public void GetPercentileAtOrBelowValue_IsMonotonic()
427+
{
428+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
429+
foreach (var v in new long[] { 1, 50, 100, 500, 1000 })
430+
{
431+
histogram.RecordValue(v);
432+
}
433+
var queryValues = new long[] { 1, 10, 50, 100, 500, 1000, 5000 };
434+
double previous = 0.0;
435+
foreach (var q in queryValues)
436+
{
437+
var current = histogram.GetPercentileAtOrBelowValue(q);
438+
current.Should().BeGreaterThanOrEqualTo(previous);
439+
previous = current;
440+
}
441+
}
442+
443+
[Fact]
444+
public void GetPercentileAtOrBelowValue_SingleRecordedValue_QueriesBelow_Return0()
445+
{
446+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
447+
histogram.RecordValue(TestValueLevel);
448+
histogram.GetPercentileAtOrBelowValue(TestValueLevel - 1).Should().Be(0.0);
449+
}
450+
451+
[Fact]
452+
public void GetPercentileAtOrBelowValue_SingleRecordedValue_QueriesAtOrAbove_Return100()
453+
{
454+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
455+
histogram.RecordValue(TestValueLevel);
456+
histogram.GetPercentileAtOrBelowValue(TestValueLevel).Should().Be(100.0);
457+
histogram.GetPercentileAtOrBelowValue(TestValueLevel + 1000).Should().Be(100.0);
458+
}
459+
460+
[Fact]
461+
public void GetPercentileAtOrBelowValue_BoundaryAtLowestTrackableValue()
462+
{
463+
var histogram = Create(1, DefaultHighestTrackableValue, DefaultSignificantFigures);
464+
histogram.RecordValue(1);
465+
histogram.RecordValue(1000);
466+
histogram.GetPercentileAtOrBelowValue(1).Should().BeApproximately(50.0, 0.1);
467+
}
468+
469+
[Fact]
470+
public void GetPercentileAtOrBelowValue_LargeMagnitudeValues_ReturnsCorrectPercentile()
471+
{
472+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
473+
histogram.RecordValue(1000);
474+
histogram.RecordValue(1_000_000);
475+
// 1000 is 50% of the recorded values
476+
histogram.GetPercentileAtOrBelowValue(1000).Should().BeApproximately(50.0, 0.1);
477+
// 1_000_000 is 100% of the recorded values
478+
histogram.GetPercentileAtOrBelowValue(1_000_000).Should().BeApproximately(100.0, 0.1);
479+
// Something between 1000 and 1_000_000 is still only 50%
480+
histogram.GetPercentileAtOrBelowValue(500_000).Should().BeApproximately(50.0, 0.1);
481+
}
482+
483+
[Fact]
484+
public void GetPercentileAtOrBelowValue_ZeroValue_WithPositiveRecordedValues_ReturnsZero()
485+
{
486+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
487+
histogram.RecordValue(100);
488+
histogram.RecordValue(1000);
489+
// No recorded values are <= 0, so should return 0.0
490+
histogram.GetPercentileAtOrBelowValue(0).Should().Be(0.0);
491+
}
492+
493+
[Fact]
494+
public void GetPercentileAtOrBelowValue_NegativeValue_ReturnsZero()
495+
{
496+
var histogram = Create(DefaultHighestTrackableValue, DefaultSignificantFigures);
497+
histogram.RecordValue(100);
498+
histogram.RecordValue(1000);
499+
// Negative values are below any trackable range, so should return 0.0
500+
histogram.GetPercentileAtOrBelowValue(-1).Should().Be(0.0);
501+
histogram.GetPercentileAtOrBelowValue(long.MinValue).Should().Be(0.0);
502+
}
503+
364504
private void CreateAndAdd(HistogramBase source)
365505
{
366506
source.RecordValueWithCount(1, 100);

HdrHistogram/HistogramBase.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,42 @@ public long GetValueAtPercentile(double percentile)
387387
throw new ArgumentOutOfRangeException(nameof(percentile), "percentile value not found in range"); // should not reach here.
388388
}
389389

390+
/// <summary>
391+
/// Get the percentile at or below a given value.
392+
/// This is the inverse of <see cref="GetValueAtPercentile"/>.
393+
/// All values within the same equivalent-value range map to the same percentile.
394+
/// </summary>
395+
/// <param name="value">The value to find the percentile for.</param>
396+
/// <returns>The percentage of recorded values that are less than or equal to <paramref name="value"/>, in the range <c>[0.0, 100.0]</c>. Returns <c>0.0</c> if no values have been recorded, or <c>100.0</c> if the value is at or above the highest trackable value.</returns>
397+
public double GetPercentileAtOrBelowValue(long value)
398+
{
399+
if (TotalCount == 0)
400+
{
401+
return 0.0;
402+
}
403+
if (value < 0)
404+
{
405+
return 0.0;
406+
}
407+
var bucketIndex = GetBucketIndex(value);
408+
var subBucketIndex = GetSubBucketIndex(value, bucketIndex);
409+
if (bucketIndex >= BucketCount)
410+
{
411+
return 100.0;
412+
}
413+
long runningCount = 0;
414+
for (var i = 0; i <= bucketIndex; i++)
415+
{
416+
var jStart = (i == 0) ? 0 : (SubBucketCount / 2);
417+
var jEnd = (i == bucketIndex) ? subBucketIndex : (SubBucketCount - 1);
418+
for (var j = jStart; j <= jEnd; j++)
419+
{
420+
runningCount += GetCountAt(i, j);
421+
}
422+
}
423+
return Math.Min(100.0, (100.0 * runningCount) / TotalCount);
424+
}
425+
390426
/// <summary>
391427
/// Get the count of recorded values at a specific value
392428
/// </summary>

0 commit comments

Comments
 (0)