Skip to content

Commit f3eadce

Browse files
committed
feat(Heap): enhance element search methods to support custom comparison functions
Should fix: #669
1 parent bf3d739 commit f3eadce

File tree

2 files changed

+127
-2
lines changed

2 files changed

+127
-2
lines changed

src/Heap.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ export class Heap<T> implements Iterable<T> {
439439
}
440440

441441
/**
442-
* Get the index of the first occurrence of the element in the heap (using the comparator).
442+
* Get the index of the first occurrence of the element in the heap.
443443
* @param {any} element Element to be found
444444
* @param {Function} callbackFn Optional comparison function, receives (element, needle)
445445
* @return {Number} Index or -1 if not found
@@ -448,6 +448,17 @@ export class Heap<T> implements Iterable<T> {
448448
if (this.heapArray.length === 0) {
449449
return -1;
450450
}
451+
// When a custom callback is provided, we must search all elements
452+
// because the callback may not be consistent with this.compare
453+
if (callbackFn !== Heap.defaultIsEqual) {
454+
for (let i = 0; i < this.heapArray.length; i++) {
455+
if (callbackFn(this.heapArray[i], element)) {
456+
return i;
457+
}
458+
}
459+
return -1;
460+
}
461+
// Default case: use heap structure optimization
451462
const indexes: number[] = [];
452463
let currentIndex = 0;
453464
while (currentIndex < this.heapArray.length) {
@@ -463,7 +474,7 @@ export class Heap<T> implements Iterable<T> {
463474
}
464475

465476
/**
466-
* Get the indexes of the every occurrence of the element in the heap (using the comparator).
477+
* Get the indexes of every occurrence of the element in the heap.
467478
* @param {any} element Element to be found
468479
* @param {Function} callbackFn Optional comparison function, receives (element, needle)
469480
* @return {Array} Array of indexes or empty array if not found
@@ -472,6 +483,18 @@ export class Heap<T> implements Iterable<T> {
472483
if (this.heapArray.length === 0) {
473484
return [];
474485
}
486+
// When a custom callback is provided, we must search all elements
487+
// because the callback may not be consistent with this.compare
488+
if (callbackFn !== Heap.defaultIsEqual) {
489+
const foundIndexes: number[] = [];
490+
for (let i = 0; i < this.heapArray.length; i++) {
491+
if (callbackFn(this.heapArray[i], element)) {
492+
foundIndexes.push(i);
493+
}
494+
}
495+
return foundIndexes;
496+
}
497+
// Default case: use heap structure optimization
475498
const indexes: number[] = [];
476499
const foundIndexes: number[] = [];
477500
let currentIndex = 0;
@@ -618,6 +641,25 @@ export class Heap<T> implements Iterable<T> {
618641
this.pop();
619642
return true;
620643
}
644+
// When a custom callback is provided, we must search all elements
645+
// because the callback may not be consistent with this.compare
646+
if (callbackFn !== Heap.defaultIsEqual) {
647+
const idx = this.indexOf(o, callbackFn);
648+
if (idx === -1) {
649+
return false;
650+
}
651+
if (idx === 0) {
652+
this.pop();
653+
} else if (idx === this.heapArray.length - 1) {
654+
this.heapArray.pop();
655+
} else {
656+
this.heapArray.splice(idx, 1, this.heapArray.pop() as T);
657+
this._sortNodeUp(idx);
658+
this._sortNodeDown(idx);
659+
}
660+
return true;
661+
}
662+
// Default case: use heap structure optimization
621663
const queue = [0];
622664
while (queue.length) {
623665
const idx = queue.shift() as number;

tests/heap/heap-public-methods.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,3 +641,86 @@ describe('Heap instances', function () {
641641
});
642642
});
643643
});
644+
645+
describe('Heap with custom IsEqual function', function () {
646+
describe('when using object heap with custom IsEqual', function () {
647+
let heap: Heap<{ value: number }>;
648+
649+
beforeEach(function () {
650+
heap = new Heap<{ value: number }>((a, b) => b.value - a.value);
651+
heap.add({ value: 1 });
652+
heap.add({ value: 2 });
653+
heap.add({ value: 3 });
654+
});
655+
656+
describe('#contains(element, fn)', function () {
657+
it('should find element using custom IsEqual that ignores the needle parameter', function () {
658+
// Using a custom IsEqual that only checks the element, ignoring the needle
659+
expect(heap.contains(0 as any, (a) => a.value === 2)).toBe(true);
660+
expect(heap.contains(0 as any, (a) => a.value === 1)).toBe(true);
661+
expect(heap.contains(0 as any, (a) => a.value === 3)).toBe(true);
662+
expect(heap.contains(0 as any, (a) => a.value === 99)).toBe(false);
663+
});
664+
});
665+
666+
describe('#indexOf(element, fn)', function () {
667+
it('should find index using custom IsEqual that ignores the needle parameter', function () {
668+
// Should find the element regardless of what needle is passed
669+
expect(heap.indexOf(0 as any, (a) => a.value === 2)).not.toBe(-1);
670+
expect(heap.indexOf(0 as any, (a) => a.value === 1)).not.toBe(-1);
671+
expect(heap.indexOf(0 as any, (a) => a.value === 3)).not.toBe(-1);
672+
expect(heap.indexOf(0 as any, (a) => a.value === 99)).toBe(-1);
673+
});
674+
675+
it('should find correct element at returned index', function () {
676+
const idx = heap.indexOf(0 as any, (a) => a.value === 2);
677+
expect(heap.heapArray[idx].value).toBe(2);
678+
});
679+
});
680+
681+
describe('#indexOfEvery(element, fn)', function () {
682+
it('should find all indexes using custom IsEqual', function () {
683+
heap.add({ value: 2 }); // Add duplicate
684+
const indexes = heap.indexOfEvery(0 as any, (a) => a.value === 2);
685+
expect(indexes.length).toBe(2);
686+
for (const idx of indexes) {
687+
expect(heap.heapArray[idx].value).toBe(2);
688+
}
689+
});
690+
});
691+
692+
describe('#remove(element, fn)', function () {
693+
it('should remove element using custom IsEqual that ignores the needle parameter', function () {
694+
// This was the failing case from the issue
695+
expect(heap.remove(0 as any, (a) => a.value === 2)).toBe(true);
696+
expect(heap.length).toBe(2);
697+
expect(heap.contains(0 as any, (a) => a.value === 2)).toBe(false);
698+
expect(heap.check()).not.toBeDefined();
699+
});
700+
701+
it('should remove any element regardless of heap position', function () {
702+
// Test removing elements at different positions
703+
const heap2 = new Heap<{ value: number }>((a, b) => b.value - a.value);
704+
heap2.add({ value: 1 });
705+
heap2.add({ value: 2 });
706+
heap2.add({ value: 3 });
707+
708+
expect(heap2.remove(0 as any, (a) => a.value === 1)).toBe(true);
709+
expect(heap2.check()).not.toBeDefined();
710+
711+
const heap3 = new Heap<{ value: number }>((a, b) => b.value - a.value);
712+
heap3.add({ value: 1 });
713+
heap3.add({ value: 2 });
714+
heap3.add({ value: 3 });
715+
716+
expect(heap3.remove(0 as any, (a) => a.value === 3)).toBe(true);
717+
expect(heap3.check()).not.toBeDefined();
718+
});
719+
720+
it('should return false when element is not found', function () {
721+
expect(heap.remove(0 as any, (a) => a.value === 99)).toBe(false);
722+
expect(heap.length).toBe(3);
723+
});
724+
});
725+
});
726+
});

0 commit comments

Comments
 (0)