Skip to content

Commit 80ec4e0

Browse files
committed
✨ feat(mq-lang): add percentile builtin function
Add `percentile(arr, p)` that calculates the p-th percentile of a numeric array using linear interpolation between closest ranks. Returns None for empty arrays and errors on invalid input.
1 parent 5bb4b48 commit 80ec4e0

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed

crates/mq-lang/builtin.mq

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,3 +626,25 @@ def slugify(s, separator = "-"):
626626
| gsub("^-+|-+$", "")
627627
end
628628

629+
# Calculates the p-th percentile of an array of numbers using linear interpolation between closest ranks.
630+
def percentile(arr, p):
631+
if (not(is_array(arr))):
632+
error("first argument must be an array")
633+
elif (is_empty(arr)):
634+
None
635+
elif (p < 0 || p > 1):
636+
error("p must be between 0 and 1")
637+
else:
638+
do
639+
let sorted = sort(arr)
640+
| let rank = p * (len(sorted) - 1)
641+
| let lower_index = floor(rank)
642+
| let upper_index = ceil(rank)
643+
| let weight = rank - lower_index
644+
| if (upper_index >= len(sorted)):
645+
sorted[lower_index]
646+
else:
647+
sorted[lower_index] * (1 - weight) + sorted[upper_index] * weight
648+
end
649+
end
650+

crates/mq-lang/builtin_tests.mq

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,44 @@ def test_index_by():
552552
| assert_eq(result2, dict())
553553
end
554554

555+
def test_percentile():
556+
# median of odd-length array
557+
let result1 = percentile([1, 2, 3, 4, 5], 0.5)
558+
| assert_eq(result1, 3.0)
559+
560+
# median of even-length array (linear interpolation)
561+
| let result2 = percentile([1, 2, 3, 4], 0.5)
562+
| assert_eq(result2, 2.5)
563+
564+
# 0th percentile = min
565+
| let result3 = percentile([3, 1, 4, 1, 5], 0)
566+
| assert_eq(result3, 1.0)
567+
568+
# 100th percentile = max
569+
| let result4 = percentile([3, 1, 4, 1, 5], 1)
570+
| assert_eq(result4, 5.0)
571+
572+
# 25th percentile
573+
| let result5 = percentile([1, 2, 3, 4], 0.25)
574+
| assert_eq(result5, 1.75)
575+
576+
# 75th percentile
577+
| let result6 = percentile([1, 2, 3, 4], 0.75)
578+
| assert_eq(result6, 3.25)
579+
580+
# single element array
581+
| let result7 = percentile([42], 0.5)
582+
| assert_eq(result7, 42.0)
583+
584+
# unsorted input is handled correctly
585+
| let result8 = percentile([5, 1, 3], 0.5)
586+
| assert_eq(result8, 3.0)
587+
588+
# empty array returns None
589+
| let result9 = percentile([], 0.5)
590+
| assert_eq(result9, None)
591+
end
592+
555593
def test_inspect():
556594
let v = 1
557595
| let result1 = inspect(v)
@@ -681,4 +719,5 @@ end
681719
test_case("between", test_between),
682720
test_case("sum_by", test_sum_by),
683721
test_case("index_by", test_index_by),
722+
test_case("percentile", test_percentile),
684723
])

0 commit comments

Comments
 (0)