mirror of
https://github.com/TheAlgorithms/JavaScript.git
synced 2025-07-05 08:16:50 +08:00
fix: refactor PrimMST and fix bug in PriorityQueue (#1300)
* ref: KeyPriorityQueue in separate file #1298 * feat: add tests for KeyPriorityQueue #1298 * fix: _shiftDown refactored and corrected #1298 * fix: use KeyPriorityQueue in PrimMST #1298 * feat: add test for PrimMST #1298 * fix: format files #1298 * fix: minor coding style changes * fix: use map for keys and priorities #1298
This commit is contained in:
153
Data-Structures/Heap/KeyPriorityQueue.js
Normal file
153
Data-Structures/Heap/KeyPriorityQueue.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* KeyPriorityQueue is a priority queue based on a Minimum Binary Heap.
|
||||||
|
*
|
||||||
|
* Minimum Binary Heaps are binary trees which are filled level by level
|
||||||
|
* and then from left to right inside a depth level.
|
||||||
|
* Their main property is that any parent node has a smaller or equal priority to all of its children,
|
||||||
|
* hence the root of the tree always has the smallest priority of all nodes.
|
||||||
|
*
|
||||||
|
* This implementation of the Minimum Binary Heap allows for nodes to be associated to both a key,
|
||||||
|
* which can be any datatype, and a priority.
|
||||||
|
*
|
||||||
|
* The heap is represented by an array with nodes ordered
|
||||||
|
* from root-to-leaf, left-to-right.
|
||||||
|
* Therefore, the parent-child node relationship is such that
|
||||||
|
* * the children nodes positions relative to their parent are: (parentPos * 2 + 1) and (parentPos * 2 + 2)
|
||||||
|
* * the parent node position relative to either of its children is: Math.floor((childPos - 1) / 2)
|
||||||
|
*
|
||||||
|
* More information and visuals on Binary Heaps can be found here: https://www.geeksforgeeks.org/binary-heap/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Priority Queue Helper functions
|
||||||
|
const getParentPosition = position => Math.floor((position - 1) / 2)
|
||||||
|
const getChildrenPositions = position => [2 * position + 1, 2 * position + 2]
|
||||||
|
|
||||||
|
class KeyPriorityQueue {
|
||||||
|
// Priority Queue class using Minimum Binary Heap
|
||||||
|
constructor () {
|
||||||
|
this._heap = []
|
||||||
|
this.priorities = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the heap is empty
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
isEmpty () {
|
||||||
|
return this._heap.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an element to the queue
|
||||||
|
* @param {*} key
|
||||||
|
* @param {number} priority
|
||||||
|
*/
|
||||||
|
push (key, priority) {
|
||||||
|
this._heap.push(key)
|
||||||
|
this.priorities.set(key, priority)
|
||||||
|
this._shiftUp(this._heap.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the element with least priority
|
||||||
|
* @returns the key of the element with least priority
|
||||||
|
*/
|
||||||
|
pop () {
|
||||||
|
this._swap(0, this._heap.length - 1)
|
||||||
|
const key = this._heap.pop()
|
||||||
|
this.priorities.delete(key)
|
||||||
|
this._shiftDown(0)
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a given key is present in the queue
|
||||||
|
* @param {*} key
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
contains (key) {
|
||||||
|
return this.priorities.has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the priority of the given element.
|
||||||
|
* Adds the element if it is not in the queue.
|
||||||
|
* @param {*} key the element to change
|
||||||
|
* @param {number} priority new priority of the element
|
||||||
|
*/
|
||||||
|
update (key, priority) {
|
||||||
|
const currPos = this._heap.indexOf(key)
|
||||||
|
// if the key does not exist yet, add it
|
||||||
|
if (currPos === -1) return this.push(key, priority)
|
||||||
|
// else update priority
|
||||||
|
this.priorities.set(key, priority)
|
||||||
|
const parentPos = getParentPosition(currPos)
|
||||||
|
const currPriority = this._getPriorityOrInfinite(currPos)
|
||||||
|
const parentPriority = this._getPriorityOrInfinite(parentPos)
|
||||||
|
const [child1Pos, child2Pos] = getChildrenPositions(currPos)
|
||||||
|
const child1Priority = this._getPriorityOrInfinite(child1Pos)
|
||||||
|
const child2Priority = this._getPriorityOrInfinite(child2Pos)
|
||||||
|
|
||||||
|
if (parentPos >= 0 && parentPriority > currPriority) {
|
||||||
|
this._shiftUp(currPos)
|
||||||
|
} else if (child1Priority < currPriority || child2Priority < currPriority) {
|
||||||
|
this._shiftDown(currPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPriorityOrInfinite (position) {
|
||||||
|
// Helper function, returns priority of the node, or Infinite if no node corresponds to this position
|
||||||
|
if (position >= 0 && position < this._heap.length) return this.priorities.get(this._heap[position])
|
||||||
|
else return Infinity
|
||||||
|
}
|
||||||
|
|
||||||
|
_shiftUp (position) {
|
||||||
|
// Helper function to shift up a node to proper position (equivalent to bubbleUp)
|
||||||
|
let currPos = position
|
||||||
|
let parentPos = getParentPosition(currPos)
|
||||||
|
let currPriority = this._getPriorityOrInfinite(currPos)
|
||||||
|
let parentPriority = this._getPriorityOrInfinite(parentPos)
|
||||||
|
|
||||||
|
while (parentPos >= 0 && parentPriority > currPriority) {
|
||||||
|
this._swap(currPos, parentPos)
|
||||||
|
currPos = parentPos
|
||||||
|
parentPos = getParentPosition(currPos)
|
||||||
|
currPriority = this._getPriorityOrInfinite(currPos)
|
||||||
|
parentPriority = this._getPriorityOrInfinite(parentPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_shiftDown (position) {
|
||||||
|
// Helper function to shift down a node to proper position (equivalent to bubbleDown)
|
||||||
|
let currPos = position
|
||||||
|
let [child1Pos, child2Pos] = getChildrenPositions(currPos)
|
||||||
|
let child1Priority = this._getPriorityOrInfinite(child1Pos)
|
||||||
|
let child2Priority = this._getPriorityOrInfinite(child2Pos)
|
||||||
|
let currPriority = this._getPriorityOrInfinite(currPos)
|
||||||
|
|
||||||
|
if (currPriority === Infinity) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while (child1Priority < currPriority || child2Priority < currPriority) {
|
||||||
|
if (child1Priority < currPriority && child1Priority < child2Priority) {
|
||||||
|
this._swap(child1Pos, currPos)
|
||||||
|
currPos = child1Pos
|
||||||
|
} else {
|
||||||
|
this._swap(child2Pos, currPos)
|
||||||
|
currPos = child2Pos
|
||||||
|
}
|
||||||
|
[child1Pos, child2Pos] = getChildrenPositions(currPos)
|
||||||
|
child1Priority = this._getPriorityOrInfinite(child1Pos)
|
||||||
|
child2Priority = this._getPriorityOrInfinite(child2Pos)
|
||||||
|
currPriority = this._getPriorityOrInfinite(currPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_swap (position1, position2) {
|
||||||
|
// Helper function to swap 2 nodes
|
||||||
|
[this._heap[position1], this._heap[position2]] = [this._heap[position2], this._heap[position1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { KeyPriorityQueue }
|
126
Data-Structures/Heap/test/KeyPriorityQueue.test.js
Normal file
126
Data-Structures/Heap/test/KeyPriorityQueue.test.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { KeyPriorityQueue } from '../KeyPriorityQueue.js'
|
||||||
|
|
||||||
|
describe('Key Priority Queue', () => {
|
||||||
|
describe('Method isEmpty', () => {
|
||||||
|
test('Check heap is empty', () => {
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
const res = queue.isEmpty()
|
||||||
|
expect(res).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Check heap is not empty', () => {
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
queue.push(0, 2)
|
||||||
|
const res = queue.isEmpty()
|
||||||
|
expect(res).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Methods push and pop', () => {
|
||||||
|
test('Test Case 1', () => {
|
||||||
|
// create queue
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
queue.push(0, 3)
|
||||||
|
queue.push(1, 7)
|
||||||
|
queue.push(2, 9)
|
||||||
|
queue.push(3, 13)
|
||||||
|
// create expected queue
|
||||||
|
const expectedQueue = new KeyPriorityQueue()
|
||||||
|
expectedQueue.push(1, 7)
|
||||||
|
expectedQueue.push(3, 13)
|
||||||
|
expectedQueue.push(2, 9)
|
||||||
|
// result from popping element from the queue
|
||||||
|
queue.pop()
|
||||||
|
expect(queue).toEqual(expectedQueue)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test Case 2', () => {
|
||||||
|
// create queue
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
queue.push(0, 3)
|
||||||
|
queue.push(1, 9)
|
||||||
|
queue.push(2, 7)
|
||||||
|
queue.push(3, 13)
|
||||||
|
// create expected queue
|
||||||
|
const expectedQueue = new KeyPriorityQueue()
|
||||||
|
expectedQueue.push(2, 7)
|
||||||
|
expectedQueue.push(1, 9)
|
||||||
|
expectedQueue.push(3, 13)
|
||||||
|
// result from popping element from the queue
|
||||||
|
queue.pop()
|
||||||
|
expect(queue).toEqual(expectedQueue)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test Case 3', () => {
|
||||||
|
// create queue
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
queue.push(0, 3)
|
||||||
|
queue.push(1, 7)
|
||||||
|
queue.push(2, 9)
|
||||||
|
queue.push(3, 12)
|
||||||
|
queue.push(4, 13)
|
||||||
|
// create expected queue
|
||||||
|
const expectedQueue = new KeyPriorityQueue()
|
||||||
|
expectedQueue.push(1, 7)
|
||||||
|
expectedQueue.push(3, 12)
|
||||||
|
expectedQueue.push(2, 9)
|
||||||
|
expectedQueue.push(4, 13)
|
||||||
|
// result from popping element from the queue
|
||||||
|
queue.pop()
|
||||||
|
expect(queue).toEqual(expectedQueue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Method contains', () => {
|
||||||
|
test('Check heap does not contain element', () => {
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
const res = queue.contains(0)
|
||||||
|
expect(res).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Check heap contains element', () => {
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
queue.push(0, 2)
|
||||||
|
const res = queue.contains(0)
|
||||||
|
expect(res).toEqual(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Method update', () => {
|
||||||
|
test('Update without change in position', () => {
|
||||||
|
// create queue
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
queue.push(0, 3)
|
||||||
|
queue.push(1, 5)
|
||||||
|
queue.push(2, 7)
|
||||||
|
queue.push(3, 11)
|
||||||
|
// create expected queue
|
||||||
|
const expectedQueue = new KeyPriorityQueue()
|
||||||
|
expectedQueue.push(0, 2)
|
||||||
|
expectedQueue.push(1, 5)
|
||||||
|
expectedQueue.push(2, 7)
|
||||||
|
expectedQueue.push(3, 11)
|
||||||
|
// result from updating to similar priority
|
||||||
|
queue.update(0, 2)
|
||||||
|
expect(queue).toEqual(expectedQueue)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Update with change in position', () => {
|
||||||
|
// create queue
|
||||||
|
const queue = new KeyPriorityQueue()
|
||||||
|
queue.push(0, 3)
|
||||||
|
queue.push(1, 5)
|
||||||
|
queue.push(2, 7)
|
||||||
|
queue.push(3, 11)
|
||||||
|
// create expected queue
|
||||||
|
const expectedQueue = new KeyPriorityQueue()
|
||||||
|
expectedQueue.push(1, 5)
|
||||||
|
expectedQueue.push(3, 11)
|
||||||
|
expectedQueue.push(2, 7)
|
||||||
|
expectedQueue.push(0, 9)
|
||||||
|
// result from updating to similar priority
|
||||||
|
queue.update(0, 9)
|
||||||
|
expect(queue).toEqual(expectedQueue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -1,148 +1,4 @@
|
|||||||
// Priority Queue Helper functions
|
import { KeyPriorityQueue } from '../Data-Structures/Heap/KeyPriorityQueue'
|
||||||
function getParentPosition (position) {
|
|
||||||
// Get the parent node of the current node
|
|
||||||
return Math.floor((position - 1) / 2)
|
|
||||||
}
|
|
||||||
function getChildrenPosition (position) {
|
|
||||||
// Get the children nodes of the current node
|
|
||||||
return [2 * position + 1, 2 * position + 2]
|
|
||||||
}
|
|
||||||
|
|
||||||
class PriorityQueue {
|
|
||||||
// Priority Queue class using Minimum Binary Heap
|
|
||||||
constructor () {
|
|
||||||
this._heap = []
|
|
||||||
this.keys = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty () {
|
|
||||||
// Checking if the heap is empty
|
|
||||||
return this._heap.length === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
push (key, priority) {
|
|
||||||
// Adding element to the queue (equivalent to add)
|
|
||||||
this._heap.push([key, priority])
|
|
||||||
this.keys[key] = this._heap.length - 1
|
|
||||||
this._shiftUp(this.keys[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
pop () {
|
|
||||||
// Removing the element with least priority (equivalent to extractMin)
|
|
||||||
this._swap(0, this._heap.length - 1)
|
|
||||||
const [key] = this._heap.pop()
|
|
||||||
delete this.keys[key]
|
|
||||||
this._shiftDown(0)
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
contains (key) {
|
|
||||||
// Check if a given key is present in the queue
|
|
||||||
return (key in this.keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
update (key, priority) {
|
|
||||||
// Update the priority of the given element (equivalent to decreaseKey)
|
|
||||||
const currPos = this.keys[key]
|
|
||||||
this._heap[currPos][1] = priority
|
|
||||||
const parentPos = getParentPosition(currPos)
|
|
||||||
const currPriority = this._heap[currPos][1]
|
|
||||||
let parentPriority = Infinity
|
|
||||||
if (parentPos >= 0) {
|
|
||||||
parentPriority = this._heap[parentPos][1]
|
|
||||||
}
|
|
||||||
const [child1Pos, child2Pos] = getChildrenPosition(currPos)
|
|
||||||
let [child1Priority, child2Priority] = [Infinity, Infinity]
|
|
||||||
if (child1Pos < this._heap.length) {
|
|
||||||
child1Priority = this._heap[child1Pos][1]
|
|
||||||
}
|
|
||||||
if (child2Pos < this._heap.length) {
|
|
||||||
child2Priority = this._heap[child2Pos][1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentPos >= 0 && parentPriority > currPriority) {
|
|
||||||
this._shiftUp(currPos)
|
|
||||||
} else if (child2Pos < this._heap.length &&
|
|
||||||
(child1Priority < currPriority || child2Priority < currPriority)) {
|
|
||||||
this._shiftDown(currPos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_shiftUp (position) {
|
|
||||||
// Helper function to shift up a node to proper position (equivalent to bubbleUp)
|
|
||||||
let currPos = position
|
|
||||||
let parentPos = getParentPosition(currPos)
|
|
||||||
let currPriority = this._heap[currPos][1]
|
|
||||||
let parentPriority = Infinity
|
|
||||||
if (parentPos >= 0) {
|
|
||||||
parentPriority = this._heap[parentPos][1]
|
|
||||||
}
|
|
||||||
|
|
||||||
while (parentPos >= 0 && parentPriority > currPriority) {
|
|
||||||
this._swap(currPos, parentPos)
|
|
||||||
currPos = parentPos
|
|
||||||
parentPos = getParentPosition(currPos)
|
|
||||||
currPriority = this._heap[currPos][1]
|
|
||||||
try {
|
|
||||||
parentPriority = this._heap[parentPos][1]
|
|
||||||
} catch (error) {
|
|
||||||
parentPriority = Infinity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.keys[this._heap[currPos][0]] = currPos
|
|
||||||
}
|
|
||||||
|
|
||||||
_shiftDown (position) {
|
|
||||||
// Helper function to shift down a node to proper position (equivalent to bubbleDown)
|
|
||||||
let currPos = position
|
|
||||||
let [child1Pos, child2Pos] = getChildrenPosition(currPos)
|
|
||||||
let [child1Priority, child2Priority] = [Infinity, Infinity]
|
|
||||||
if (child1Pos < this._heap.length) {
|
|
||||||
child1Priority = this._heap[child1Pos][1]
|
|
||||||
}
|
|
||||||
if (child2Pos < this._heap.length) {
|
|
||||||
child2Priority = this._heap[child2Pos][1]
|
|
||||||
}
|
|
||||||
let currPriority
|
|
||||||
try {
|
|
||||||
currPriority = this._heap[currPos][1]
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
while (child2Pos < this._heap.length &&
|
|
||||||
(child1Priority < currPriority || child2Priority < currPriority)) {
|
|
||||||
if (child1Priority < currPriority && child1Priority < child2Priority) {
|
|
||||||
this._swap(child1Pos, currPos)
|
|
||||||
currPos = child1Pos
|
|
||||||
} else {
|
|
||||||
this._swap(child2Pos, currPos)
|
|
||||||
currPos = child2Pos
|
|
||||||
}
|
|
||||||
[child1Pos, child2Pos] = getChildrenPosition(currPos)
|
|
||||||
try {
|
|
||||||
[child1Priority, child2Priority] = [this._heap[child1Pos][1], this._heap[child2Pos][1]]
|
|
||||||
} catch (error) {
|
|
||||||
[child1Priority, child2Priority] = [Infinity, Infinity]
|
|
||||||
}
|
|
||||||
|
|
||||||
currPriority = this._heap[currPos][1]
|
|
||||||
}
|
|
||||||
this.keys[this._heap[currPos][0]] = currPos
|
|
||||||
if (child1Pos < this._heap.length && child1Priority < currPriority) {
|
|
||||||
this._swap(child1Pos, currPos)
|
|
||||||
this.keys[this._heap[child1Pos][0]] = child1Pos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_swap (position1, position2) {
|
|
||||||
// Helper function to swap 2 nodes
|
|
||||||
[this._heap[position1], this._heap[position2]] = [this._heap[position2], this._heap[position1]]
|
|
||||||
this.keys[this._heap[position1][0]] = position1
|
|
||||||
this.keys[this._heap[position2][0]] = position2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GraphWeightedUndirectedAdjacencyList {
|
class GraphWeightedUndirectedAdjacencyList {
|
||||||
// Weighted Undirected Graph class
|
// Weighted Undirected Graph class
|
||||||
constructor () {
|
constructor () {
|
||||||
@ -167,7 +23,7 @@ class GraphWeightedUndirectedAdjacencyList {
|
|||||||
// Details: https://en.wikipedia.org/wiki/Prim%27s_algorithm
|
// Details: https://en.wikipedia.org/wiki/Prim%27s_algorithm
|
||||||
const distance = {}
|
const distance = {}
|
||||||
const parent = {}
|
const parent = {}
|
||||||
const priorityQueue = new PriorityQueue()
|
const priorityQueue = new KeyPriorityQueue()
|
||||||
// Initialization
|
// Initialization
|
||||||
for (const node in this.connections) {
|
for (const node in this.connections) {
|
||||||
distance[node] = (node === start.toString() ? 0 : Infinity)
|
distance[node] = (node === start.toString() ? 0 : Infinity)
|
||||||
@ -198,11 +54,3 @@ class GraphWeightedUndirectedAdjacencyList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { GraphWeightedUndirectedAdjacencyList }
|
export { GraphWeightedUndirectedAdjacencyList }
|
||||||
|
|
||||||
// const graph = new GraphWeightedUndirectedAdjacencyList()
|
|
||||||
// graph.addEdge(1, 2, 1)
|
|
||||||
// graph.addEdge(2, 3, 2)
|
|
||||||
// graph.addEdge(3, 4, 1)
|
|
||||||
// graph.addEdge(3, 5, 100) // Removed in MST
|
|
||||||
// graph.addEdge(4, 5, 5)
|
|
||||||
// graph.PrimMST(1)
|
|
||||||
|
20
Graphs/test/PrimMST.test.js
Normal file
20
Graphs/test/PrimMST.test.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { GraphWeightedUndirectedAdjacencyList } from '../PrimMST.js'
|
||||||
|
|
||||||
|
test('Test Case PrimMST 1', () => {
|
||||||
|
// create graph to compute MST on
|
||||||
|
const graph = new GraphWeightedUndirectedAdjacencyList()
|
||||||
|
graph.addEdge(1, 2, 1)
|
||||||
|
graph.addEdge(2, 3, 2)
|
||||||
|
graph.addEdge(3, 4, 1)
|
||||||
|
graph.addEdge(3, 5, 100) // Removed in MST
|
||||||
|
graph.addEdge(4, 5, 5)
|
||||||
|
// create expected graph
|
||||||
|
const expectedGraph = new GraphWeightedUndirectedAdjacencyList()
|
||||||
|
expectedGraph.addEdge(1, 2, 1)
|
||||||
|
expectedGraph.addEdge(2, 3, 2)
|
||||||
|
expectedGraph.addEdge(3, 4, 1)
|
||||||
|
expectedGraph.addEdge(4, 5, 5)
|
||||||
|
// result from MST
|
||||||
|
const res = graph.PrimMST(1)
|
||||||
|
expect(res).toEqual(expectedGraph)
|
||||||
|
})
|
Reference in New Issue
Block a user