From f317926fb54ec4b0b09151fdc7745f9dabc1ab6b Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Sat, 26 Sep 2026 18:07:10 -0400 Subject: [PATCH 1/9] LRU visualization --- src/AlgoList.js | 17 +- src/algo/Algorithm.js | 132 +++-- src/algo/LRU.js | 961 +++++++++++++++++++++++++++++++++ src/algo/__tests__/LRU.test.js | 339 ++++++++++++ src/algo/index.js | 1 + src/pseudocode.json | 70 +++ src/time_complexities.json | 20 + 7 files changed, 1485 insertions(+), 55 deletions(-) create mode 100644 src/algo/LRU.js create mode 100644 src/algo/__tests__/LRU.test.js diff --git a/src/AlgoList.js b/src/AlgoList.js index 3f6ccbd..fb9ad9d 100644 --- a/src/AlgoList.js +++ b/src/AlgoList.js @@ -56,8 +56,9 @@ export const algoMap = { Kruskal: ["Kruskal's", algos.Kruskals, true], DisjointSet: ['Disjoint Set', algos.DisjointSet, true], LCS: ['LCS', algos.LCS, true, 'Longest Common Subsequence'], + LRU: ['LRU Cache', algos.LRU, true, 'LRU Cache'], Floyd: ['Floyd-Warshall', algos.Floyd], - TreeMap: ['TreeMap', algos.TreeMap] + TreeMap: ['TreeMap', algos.TreeMap], }; const aprilFoolsAlgos = [ @@ -92,7 +93,7 @@ const baseAlgoList = [ 'Maps', 'ClosedHash', 'OpenHash', - "TreeMap", + 'TreeMap', 'Sorting and Quickselect', 'BubbleSort', 'CocktailSort', @@ -120,6 +121,7 @@ const baseAlgoList = [ '---', 'DP & Extras', 'SplayTree', + 'LRU', 'Floyd', 'DropSort', 'SleepSort', @@ -197,7 +199,8 @@ export const relatedSearches = { Dijkstra: ['BFS', 'DFS', 'Prim', 'Kruskal', 'DisjointSet'], Prim: ['BFS', 'DFS', 'Dijkstra', 'Kruskal', 'DisjointSet'], Kruskal: ['BFS', 'DFS', 'Dijkstra', 'Prim', 'DisjointSet'], - LCS: ['Floyd'], + LCS: ['Floyd', 'LRU'], + LRU: ['ClosedHash', 'OpenHash', 'DoublyLinkedList', 'LCS'], Floyd: ['LCS'], TreeMap: ['AVL', 'ClosedHash', 'OpenHash'], }; @@ -351,13 +354,17 @@ export const algoFilter = [ id: 'Floyd', category: 'DP & Extras', }, + { + id: 'LRU', + category: 'DP & Extras', + }, { id: 'CreateGraph', category: 'Graphs', }, { - id: "DisjointSet", - category: 'Graphs' + id: 'DisjointSet', + category: 'Graphs', }, { id: 'TreeMap', diff --git a/src/algo/Algorithm.js b/src/algo/Algorithm.js index 70507ef..0511d8c 100644 --- a/src/algo/Algorithm.js +++ b/src/algo/Algorithm.js @@ -29,7 +29,9 @@ import { act } from '../anim/AnimationMain'; let controlTabNavigationInstalled = false; const getControlRoots = () => { - const algorithmRoot = document.getElementById('algoControlSection') || document.getElementById('AlgorithmSpecificControls'); + const algorithmRoot = + document.getElementById('algoControlSection') || + document.getElementById('AlgorithmSpecificControls'); const animationRoot = document.getElementById('GeneralAnimationControls'); const roots = [algorithmRoot, animationRoot].filter(Boolean); console.log('Control roots found:', roots.length, roots); @@ -39,56 +41,68 @@ const getControlRoots = () => { const getControlFocusables = () => { const roots = getControlRoots(); return roots - .flatMap((root) => + .flatMap(root => Array.from( root.querySelectorAll( - 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])' - ) - ) + 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])', + ), + ), ) - .filter((el) => !el.disabled && el.offsetParent !== null); + .filter(el => !el.disabled && el.offsetParent !== null); }; -const handleControlTabNavigation = (event) => { +const handleControlTabNavigation = event => { if (event.key !== 'Tab') return; - + const roots = getControlRoots(); const activeElement = document.activeElement; - + // Check if the active element is within a control root - if (!roots.some((root) => root && root.contains(activeElement))) return; + if (!roots.some(root => root && root.contains(activeElement))) return; const focusables = getControlFocusables(); - console.log('Tab pressed. Active element:', activeElement.tagName, activeElement.type, activeElement.value); + console.log( + 'Tab pressed. Active element:', + activeElement.tagName, + activeElement.type, + activeElement.value, + ); console.log('Focusables found:', focusables.length); focusables.forEach((f, i) => console.log(` [${i}]`, f.tagName, f.type, f.value)); console.log( - focusables.map(el => ({ - tag: el.tagName, - type: el.type, - visible: !!el.offsetParent, - tabIndex: el.tabIndex - })) -); - + focusables.map(el => ({ + tag: el.tagName, + type: el.type, + visible: !!el.offsetParent, + tabIndex: el.tabIndex, + })), + ); + if (focusables.length === 0) return; event.preventDefault(); const currentIndex = focusables.indexOf(activeElement); console.log('Current index in focusables:', currentIndex); - + // If active element not found, start from first const startIndex = currentIndex === -1 ? 0 : currentIndex; const direction = event.shiftKey ? -1 : 1; let nextIndex = startIndex + direction; - + if (nextIndex >= focusables.length) nextIndex = 0; if (nextIndex < 0) nextIndex = focusables.length - 1; - console.log('Moving to index:', nextIndex, 'element:', focusables[nextIndex].tagName, focusables[nextIndex].type, focusables[nextIndex].value); + console.log( + 'Moving to index:', + nextIndex, + 'element:', + focusables[nextIndex].tagName, + focusables[nextIndex].type, + focusables[nextIndex].value, + ); - focusables.forEach((el) => el.classList.remove('tab-focused-input')); + focusables.forEach(el => el.classList.remove('tab-focused-input')); const next = focusables[nextIndex]; next.focus(); next.classList.add('tab-focused-input'); @@ -179,12 +193,12 @@ export function addDropDownGroupToAlgorithmBar(optionNames, groupName, group, al group.appendChild(span); span.setAttribute('class', 'groupChild'); } - + // Attach Tab navigation if algorithm instance is provided if (algorithm) { algorithm.attachTabNavigation(dropDown); } - + return dropDown; } @@ -210,7 +224,7 @@ export function addRadioButtonGroupToAlgorithmBar(buttonNames, groupName, group, bottomLevel.appendChild(label); newTable.appendChild(midLevel); buttonList.push(button); - + // Attach Tab navigation if algorithm instance is provided if (algorithm) { algorithm.attachTabNavigation(button); @@ -250,12 +264,19 @@ export function addControlToAlgorithmBar(type, value, group, algorithm = null) { } // Add tabindex for proper keyboard focus - if (normalizedType === 'button' || normalizedType === 'checkbox' || normalizedType === 'radio') { + if ( + normalizedType === 'button' || + normalizedType === 'checkbox' || + normalizedType === 'radio' + ) { element.setAttribute('tabindex', '0'); } // Attach Tab navigation to buttons, checkboxes, radio buttons if algorithm instance is provided - if (algorithm && (normalizedType === 'button' || normalizedType === 'checkbox' || normalizedType === 'radio')) { + if ( + algorithm && + (normalizedType === 'button' || normalizedType === 'checkbox' || normalizedType === 'radio') + ) { algorithm.attachTabNavigation(element); } @@ -499,16 +520,19 @@ export default class Algorithm { } getNextFocusableInput(currentField, direction = 1) { - const algorithmRoot = document.getElementById('algoControlSection') || document.getElementById('AlgorithmSpecificControls'); + const algorithmRoot = + document.getElementById('algoControlSection') || + document.getElementById('AlgorithmSpecificControls'); const roots = [algorithmRoot, document.getElementById('GeneralAnimationControls')]; if (roots.length === 0) return null; - const allInputs = roots - .flatMap((root) => Array.from( + const allInputs = roots.flatMap(root => + Array.from( root.querySelectorAll( - 'input[type="text"], input[type="button"], input[type="checkbox"], input[type="radio"], select, [tabindex="0"]' - ) - )); + 'input[type="text"], input[type="button"], input[type="checkbox"], input[type="radio"], select, [tabindex="0"]', + ), + ), + ); if (allInputs.length === 0) return null; @@ -538,13 +562,17 @@ export default class Algorithm { } unhighlightInputFields() { - const algorithmRoot = document.getElementById('algoControlSection') || document.getElementById('AlgorithmSpecificControls'); - const roots = [algorithmRoot, document.getElementById('GeneralAnimationControls')].filter(Boolean); - roots.forEach((root) => { + const algorithmRoot = + document.getElementById('algoControlSection') || + document.getElementById('AlgorithmSpecificControls'); + const roots = [algorithmRoot, document.getElementById('GeneralAnimationControls')].filter( + Boolean, + ); + roots.forEach(root => { const allInputs = root.querySelectorAll( - 'input, button, select, textarea, [tabindex]:not([tabindex="-1"])' + 'input, button, select, textarea, [tabindex]:not([tabindex="-1"])', ); - allInputs.forEach((input) => { + allInputs.forEach(input => { input.classList.remove('tab-focused-input'); }); }); @@ -552,7 +580,7 @@ export default class Algorithm { attachTabNavigation(element, action = null) { // Attach Tab/Enter navigation to any focusable element - element.addEventListener('keydown', (event) => { + element.addEventListener('keydown', event => { const keyASCII = event.keyCode || event.which; // Handle Enter key - trigger click for buttons, or action function @@ -578,7 +606,7 @@ export default class Algorithm { if (maxsize !== undefined) { field.size = maxsize; } - return (event) => { + return event => { let keyASCII = 0; if (window.event) { // IE @@ -606,17 +634,21 @@ export default class Algorithm { !sizeGood || (intOnly && !( - (keyASCII >= 48 && keyASCII <= 57) || // 0-9 - (keyASCII === 189 && field.value.length === 0) || // minus at start - (keyASCII >= 96 && keyASCII <= 105) // numpad 0-9 + ( + (keyASCII >= 48 && keyASCII <= 57) || // 0-9 + (keyASCII === 189 && field.value.length === 0) || // minus at start + (keyASCII >= 96 && keyASCII <= 105) + ) // numpad 0-9 )) || (!intOnly && !( - (keyASCII >= 48 && keyASCII <= 57) || // 0-9 - (keyASCII >= 65 && keyASCII <= 90) || // A-Z - (keyASCII >= 97 && keyASCII <= 122) || // a-z - (keyASCII === 189 && field.value.length === 0) || // minus at start - (keyASCII >= 96 && keyASCII <= 105) // numpad 0-9 + ( + (keyASCII >= 48 && keyASCII <= 57) || // 0-9 + (keyASCII >= 65 && keyASCII <= 90) || // A-Z + (keyASCII >= 97 && keyASCII <= 122) || // a-z + (keyASCII === 189 && field.value.length === 0) || // minus at start + (keyASCII >= 96 && keyASCII <= 105) + ) // numpad 0-9 )) ) { if (!controlKey(keyASCII)) return false; @@ -666,7 +698,7 @@ Algorithm.prototype.returnSubmitFloat = function (field, funct, maxsize) { if (maxsize !== undefined) { field.size = maxsize; } - return (event) => { + return event => { let keyASCII = 0; if (window.event) { // IE diff --git a/src/algo/LRU.js b/src/algo/LRU.js new file mode 100644 index 0000000..5d2bf4e --- /dev/null +++ b/src/algo/LRU.js @@ -0,0 +1,961 @@ +// Copyright 2011 David Galles, University of San Francisco. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import Algorithm, { + addControlToAlgorithmBar, + addDivisorToAlgorithmBar, + addGroupToAlgorithmBar, + addLabelToAlgorithmBar, +} from './Algorithm.js'; +import { act } from '../anim/AnimationMain'; +import pseudocodeText from '../pseudocode.json'; + +// ─── Layout constants ──────────────────────────────────────────────────────── + +const INFO_MSG_X = 25; +const INFO_MSG_Y = 15; + +const CAP_LABEL_X = 25; +const CAP_LABEL_Y = 40; + +// Cache Table (Left side) +const MAP_TITLE_X = 25; +const MAP_TITLE_Y = 75; +const MAP_COL_KEY_X = 48; +const MAP_COL_REF_X = 98; +const MAP_COL_HEADER_Y = 100; +const MAP_START_Y = 130; +const MAP_ROW_GAP = 32; +const MAP_KEY_W = 46; +const MAP_REF_W = 34; +const MAP_ROW_H = 24; + +// Doubly Linked List (Right side) +const DLL_START_X = 220; +const DLL_START_Y = 200; +const DLL_NODE_W = 65; +const DLL_NODE_H = 30; +const DLL_X_GAP = 95; + +// Value label sits below each DLL node +const VAL_OFFSET_Y = DLL_NODE_H / 2 + 15; + +// MRU / LRU badges sit with comfortable clearance above the DLL row +const BADGE_W = 45; +const BADGE_H = 22; +const BADGE_Y = DLL_START_Y - 50; + +// Off-screen position used to "hide" canvas objects we can't destroy +const OFFSCREEN = -2000; + +// ─── Colors ────────────────────────────────────────────────────────────────── + +const MRU_COLOR = '#006400'; // dark green +const LRU_COLOR = '#CC0000'; // crimson red +const COMBO_BADGE_COLOR = '#4A148C'; // dark purple for capacity/size 1 MRU/LRU badge +const HIGHLIGHT_COLOR = '#FFD700'; // gold — insertion flash / update +const EVICT_COLOR = '#FF6666'; // red — eviction / deletion +const FOUND_COLOR = '#90EE90'; // light green — get hit +const MAP_LINK_COLOR = '#1976D2'; // blue for active Hash Map -> DLL node references +const KEY_CELL_BG = '#F8F9FA'; +const PTR_CELL_BG = '#E3F2FD'; +const CELL_BORDER = '#B0BEC5'; +const WHITE = '#FFFFFF'; + +// ─── Limits ─────────────────────────────────────────────────────────────────── + +const MAX_CAPACITY = 10; +const DEFAULT_CAPACITY = 5; +const MAX_KEY_LENGTH = 4; +const MAX_VAL_LENGTH = 4; + +// Keys that would cause prototype pollution +const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + +// ─── Status Message Helpers ─────────────────────────────────────────────────── + +export const STATUS = { + // Validation / Input errors + NEED_KEY_VALUE: 'Please enter both a key and a value.', + FORBIDDEN_KEY: key => `Key "${key}" is not allowed.`, + NEED_GET_KEY: 'Please enter a key to get.', + NEED_DEL_KEY: 'Please enter a key to delete.', + INVALID_CAPACITY: max => `Capacity must be an integer between 1 and ${max}.`, + + // Put + PUT_HIT: (key, val) => `Cache Hit on key "${key}" — updating value to "${val}".`, + PUT_EVICT: (cap, key) => `Cache full (${cap}/${cap}) — evicting LRU key "${key}".`, + PUT_INSERT: (key, val) => `Inserting "${key}" → "${val}" into Cache and DLL at MRU position.`, + PUT_COMPLETE: (key, val) => `put("${key}", "${val}") complete.`, + + // Get + GET_MISS: key => `Cache Miss: key "${key}" not found in Cache — returning -1.`, + GET_HIT: key => `Cache Hit! Key "${key}" located in Cache (O(1)) → pointing to DLL node.`, + GET_COMPLETE: (key, val) => `get("${key}") → "${val}".`, + + // Delete + DEL_MISS: key => `Key "${key}" not in Cache — nothing to delete.`, + DEL_FOUND: key => `Deleting key "${key}" from Cache and DLL.`, + DEL_COMPLETE: key => `delete("${key}") complete.`, + + // Navigation & Inspection + MOVE_TO_MRU: key => `Moving "${key}" to MRU position.`, + INSPECT: (key, val, pos) => + `Inspecting: key "${key}" maps to DLL node "${key}" (value: "${val}") at ${pos}.`, +}; + +// ─── LRU class ─────────────────────────────────────────────────────────────── + +export default class LRU extends Algorithm { + constructor(am, w, h) { + super(am, w, h); + this.addControls(); + this.nextIndex = 0; + this.commands = []; + this.inspectedKey = null; + this.listenersAttached = false; + this.setup(); + } + + // ── Control bar ──────────────────────────────────────────────────────────── + + addControls() { + this.controls = []; + + // Put: Key / Value (two-row group) + const putVG = addGroupToAlgorithmBar(false); + const putTop = addGroupToAlgorithmBar(true, putVG); + const putBot = addGroupToAlgorithmBar(true, putVG); + + addLabelToAlgorithmBar('Key:', putTop); + this.putKeyField = addControlToAlgorithmBar('Text', '', putTop); + this.putKeyField.size = MAX_KEY_LENGTH; + this.putKeyField.onkeydown = this.returnSubmit( + this.putKeyField, + this.putCallback.bind(this), + MAX_KEY_LENGTH, + false, + ); + this.controls.push(this.putKeyField); + + addLabelToAlgorithmBar('Value:', putBot); + this.putValField = addControlToAlgorithmBar('Text', '', putBot); + this.putValField.size = MAX_VAL_LENGTH; + this.putValField.onkeydown = this.returnSubmit( + this.putValField, + this.putCallback.bind(this), + MAX_VAL_LENGTH, + false, + ); + this.controls.push(this.putValField); + + this.putButton = addControlToAlgorithmBar('Button', 'Put'); + this.putButton.onclick = this.putCallback.bind(this); + this.controls.push(this.putButton); + + addDivisorToAlgorithmBar(); + + // Get + const getG = addGroupToAlgorithmBar(true); + addLabelToAlgorithmBar('Key:', getG); + this.getKeyField = addControlToAlgorithmBar('Text', '', getG); + this.getKeyField.size = MAX_KEY_LENGTH; + this.getKeyField.onkeydown = this.returnSubmit( + this.getKeyField, + this.getCallback.bind(this), + MAX_KEY_LENGTH, + false, + ); + this.controls.push(this.getKeyField); + + this.getButton = addControlToAlgorithmBar('Button', 'Get'); + this.getButton.onclick = this.getCallback.bind(this); + this.controls.push(this.getButton); + + addDivisorToAlgorithmBar(); + + // Delete + const delG = addGroupToAlgorithmBar(true); + addLabelToAlgorithmBar('Key:', delG); + this.deleteKeyField = addControlToAlgorithmBar('Text', '', delG); + this.deleteKeyField.size = MAX_KEY_LENGTH; + this.deleteKeyField.onkeydown = this.returnSubmit( + this.deleteKeyField, + this.deleteCallback.bind(this), + MAX_KEY_LENGTH, + false, + ); + this.controls.push(this.deleteKeyField); + + this.deleteButton = addControlToAlgorithmBar('Button', 'Delete'); + this.deleteButton.onclick = this.deleteCallback.bind(this); + this.controls.push(this.deleteButton); + + addDivisorToAlgorithmBar(); + + // Capacity + Restart + const capVG = addGroupToAlgorithmBar(false); + const capTop = addGroupToAlgorithmBar(true, capVG); + const capBot = addGroupToAlgorithmBar(true, capVG); + + addLabelToAlgorithmBar('Capacity:', capTop); + this.capacityField = addControlToAlgorithmBar('Text', String(DEFAULT_CAPACITY), capTop); + this.capacityField.size = 3; + this.capacityField.onkeydown = this.returnSubmit( + this.capacityField, + this.restartCallback.bind(this), + 2, + true, + ); + this.controls.push(this.capacityField); + + this.restartButton = addControlToAlgorithmBar('Button', 'Restart', capBot); + this.restartButton.onclick = this.restartCallback.bind(this); + this.controls.push(this.restartButton); + + addDivisorToAlgorithmBar(); + + // Clear button + this.clearButton = addControlToAlgorithmBar('Button', 'Clear'); + this.clearButton.onclick = this.clearCallback.bind(this); + this.controls.push(this.clearButton); + } + + // ── Setup ────────────────────────────────────────────────────────────────── + + setup() { + // JS-side state + this.capacity = DEFAULT_CAPACITY; + // O(1) lookup map: key → { keyCellID, ptrCellID, nodeID, valLabelID } + this.map = Object.create(null); + // Active keys in the Hash Map + this.mapKeys = []; + // Ordered MRU→LRU: [{ key, value, nodeID, valLabelID }] + this.dll = []; + // Tracked DLL connections + this.dllConnections = []; + + // ── Permanent canvas objects ─────────────────────────────────────────── + + this.infoLabelID = this.nextIndex++; + this.cmd(act.createLabel, this.infoLabelID, '', INFO_MSG_X, INFO_MSG_Y, 0); + + // Capacity counter text + this.capLabelID = this.nextIndex++; + this.cmd(act.createLabel, this.capLabelID, this.capText(), CAP_LABEL_X, CAP_LABEL_Y, 0); + + // Cache section title & column headers + this.mapTitleID = this.nextIndex++; + this.cmd(act.createLabel, this.mapTitleID, 'Cache', MAP_TITLE_X, MAP_TITLE_Y, 0); + this.cmd(act.setForegroundColor, this.mapTitleID, '#1976D2'); + + this.mapColKeyID = this.nextIndex++; + this.cmd(act.createLabel, this.mapColKeyID, 'Key', MAP_COL_KEY_X, MAP_COL_HEADER_Y, 1); + this.cmd(act.setForegroundColor, this.mapColKeyID, '#666666'); + + this.mapColRefID = this.nextIndex++; + this.cmd(act.createLabel, this.mapColRefID, 'Node Ref', MAP_COL_REF_X, MAP_COL_HEADER_Y, 1); + this.cmd(act.setForegroundColor, this.mapColRefID, '#666666'); + + // "val:" label sits to the left of the value row + this.valHeaderID = this.nextIndex++; + this.cmd( + act.createLabel, + this.valHeaderID, + 'val:', + DLL_START_X - 45, + DLL_START_Y + VAL_OFFSET_Y, + 0, + ); + this.cmd(act.setForegroundColor, this.valHeaderID, '#888888'); + + // MRU badge — colored rectangle + text + this.mruBadgeID = this.nextIndex++; + this.mruTextID = this.nextIndex++; + this.cmd(act.createRectangle, this.mruBadgeID, '', BADGE_W, BADGE_H, OFFSCREEN, OFFSCREEN); + this.cmd(act.setBackgroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.setForegroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.createLabel, this.mruTextID, '', OFFSCREEN, OFFSCREEN, 1); + this.cmd(act.setForegroundColor, this.mruTextID, WHITE); + + // LRU badge — colored rectangle + text + this.lruBadgeID = this.nextIndex++; + this.lruTextID = this.nextIndex++; + this.cmd(act.createRectangle, this.lruBadgeID, '', BADGE_W, BADGE_H, OFFSCREEN, OFFSCREEN); + this.cmd(act.setBackgroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.setForegroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.createLabel, this.lruTextID, '', OFFSCREEN, OFFSCREEN, 1); + this.cmd(act.setForegroundColor, this.lruTextID, WHITE); + + this.pseudocode = pseudocodeText.LRU; + + // Store nextIndex after setup so reset() can restore it + this.resetIndex = this.nextIndex; + + this.animationManager.startNewAnimation(this.commands); + this.animationManager.skipForward(); + this.animationManager.clearHistory(); + this.commands = []; + + // Attach canvas event listeners for interactive Click-to-Inspect + this.canvas = document.getElementById('canvas'); + if (this.canvas && !this.listenersAttached) { + this.canvas.addEventListener('click', this.handleCanvasClick.bind(this)); + this.canvas.addEventListener('mousemove', this.handleCanvasMouseMove.bind(this)); + this.listenersAttached = true; + } + } + + reset() { + this.map = Object.create(null); + this.mapKeys = []; + this.dll = []; + this.dllConnections = []; + this.inspectedKey = null; + this.nextIndex = this.resetIndex; + } + + // ── Interactive Click-to-Inspect ─────────────────────────────────────────── + + getCanvasCoords(event) { + if (!this.canvas) return { x: -1, y: -1 }; + const rect = this.canvas.getBoundingClientRect(); + const scaleX = this.canvas.width / rect.width; + const scaleY = this.canvas.height / rect.height; + return { + x: (event.clientX - rect.left) * scaleX, + y: (event.clientY - rect.top) * scaleY, + }; + } + + findHitRow(x, y) { + for (let i = 0; i < this.mapKeys.length; i++) { + const rowY = this.mapRowY(i); + const left = MAP_COL_KEY_X - MAP_KEY_W / 2; + const right = MAP_COL_REF_X + MAP_REF_W / 2; + const top = rowY - MAP_ROW_H / 2; + const bottom = rowY + MAP_ROW_H / 2; + if (x >= left && x <= right && y >= top && y <= bottom) { + return i; + } + } + return -1; + } + + handleCanvasMouseMove(event) { + if (!this.canvas) return; + const { x, y } = this.getCanvasCoords(event); + const hit = this.findHitRow(x, y); + this.canvas.style.cursor = hit !== -1 ? 'pointer' : 'default'; + } + + handleCanvasClick(event) { + if (this.animationManager && this.animationManager.currentlyAnimating) return; + const { x, y } = this.getCanvasCoords(event); + const hitRow = this.findHitRow(x, y); + + if (hitRow === -1) { + if (this.inspectedKey) this.clearInspection(); + return; + } + + const key = this.mapKeys[hitRow]; + if (this.inspectedKey === key) { + this.clearInspection(); + } else { + this.inspectKey(key); + } + } + + inspectKey(key) { + this.clearInspection(); + const mapEntry = this.map[key]; + const dllIdx = this.dll.findIndex(n => n.key === key); + if (!mapEntry || dllIdx === -1) return; + + const targetNode = this.dll[dllIdx]; + this.inspectedKey = key; + this.commands = []; + + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, PTR_CELL_BG); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, '#BBDEFB'); + this.cmd(act.setBackgroundColor, targetNode.nodeID, '#E1F5FE'); + this.cmd( + act.connect, + mapEntry.ptrCellID, + targetNode.nodeID, + MAP_LINK_COLOR, + 0.0, + true, + '', + 0, + ); + + const posDesc = + dllIdx === 0 + ? 'index 0 (MRU)' + : dllIdx === this.dll.length - 1 + ? `index ${dllIdx} (LRU)` + : `index ${dllIdx}`; + this.setStatus(STATUS.INSPECT(key, targetNode.value, posDesc)); + + this.animationManager.startNewAnimation(this.commands); + this.animationManager.skipForward(); + this.commands = []; + } + + clearInspection() { + if (!this.inspectedKey) return; + const key = this.inspectedKey; + const mapEntry = this.map[key]; + const dllIdx = this.dll.findIndex(n => n.key === key); + this.inspectedKey = null; + + this.commands = []; + if (mapEntry) { + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, KEY_CELL_BG); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, PTR_CELL_BG); + if (dllIdx !== -1) { + this.cmd(act.disconnect, mapEntry.ptrCellID, this.dll[dllIdx].nodeID); + this.cmd(act.setBackgroundColor, this.dll[dllIdx].nodeID, WHITE); + } + } + this.setStatus(''); + this.animationManager.startNewAnimation(this.commands); + this.animationManager.skipForward(); + this.commands = []; + } + + // ── UI lifecycle ─────────────────────────────────────────────────────────── + + enableUI() { + for (let i = 0; i < this.controls.length; i++) { + this.controls[i].disabled = false; + } + } + + disableUI() { + for (let i = 0; i < this.controls.length; i++) { + this.controls[i].disabled = true; + } + } + + // ── Helpers ──────────────────────────────────────────────────────────────── + + setStatus(text) { + this.cmd(act.setText, this.infoLabelID, text); + } + + capText() { + return `Capacity: ${this.dll ? this.dll.length : 0} / ${this.capacity}`; + } + + dllNodeX(idx) { + return DLL_START_X + idx * DLL_X_GAP; + } + + mapRowY(idx) { + return MAP_START_Y + idx * MAP_ROW_GAP; + } + + // ── Callbacks ────────────────────────────────────────────────────────────── + + putCallback() { + this.clearInspection(); + const key = this.putKeyField.value.trim(); + const val = this.putValField.value.trim(); + if (key === '' || val === '') { + this.implementAction(this.setStatusCmd.bind(this), STATUS.NEED_KEY_VALUE); + this.shake(this.putButton); + return; + } + if (FORBIDDEN_KEYS.has(key)) { + this.implementAction(this.setStatusCmd.bind(this), STATUS.FORBIDDEN_KEY(key)); + this.shake(this.putButton); + return; + } + this.putKeyField.value = ''; + this.putValField.value = ''; + this.implementAction(this.animatePut.bind(this), key, val); + } + + getCallback() { + this.clearInspection(); + const key = this.getKeyField.value.trim(); + if (key === '') { + this.implementAction(this.setStatusCmd.bind(this), STATUS.NEED_GET_KEY); + this.shake(this.getButton); + return; + } + this.getKeyField.value = ''; + this.implementAction(this.animateGet.bind(this), key); + } + + deleteCallback() { + this.clearInspection(); + const key = this.deleteKeyField.value.trim(); + if (key === '') { + this.implementAction(this.setStatusCmd.bind(this), STATUS.NEED_DEL_KEY); + this.shake(this.deleteButton); + return; + } + this.deleteKeyField.value = ''; + this.implementAction(this.animateDelete.bind(this), key); + } + + restartCallback() { + this.clearInspection(); + const raw = parseInt(this.capacityField.value, 10); + // Enforce capacity between 1 and MAX_CAPACITY (explicitly disallow 0 or negative) + if (isNaN(raw) || raw < 1 || raw > MAX_CAPACITY) { + this.implementAction( + this.setStatusCmd.bind(this), + STATUS.INVALID_CAPACITY(MAX_CAPACITY), + ); + this.shake(this.restartButton); + return; + } + this.capacity = raw; + this.implementAction(this.clearAll.bind(this)); + } + + clearCallback() { + this.clearInspection(); + this.implementAction(this.clearAll.bind(this)); + } + + setStatusCmd(text) { + this.commands = []; + this.setStatus(text); + return this.commands; + } + + // ── Animation: put ──────────────────────────────────────────────────────── + + animatePut(key, value) { + this.commands = []; + this.setStatus(''); + this.highlight(0, 0, 'put'); + this.cmd(act.step); + + if (key in this.map) { + // ── Key exists: update in map and move DLL node to head (MRU) ─── + this.highlight(1, 0, 'put'); + this.cmd(act.step); + + const mapEntry = this.map[key]; + const dllIdx = this.dll.findIndex(n => n.key === key); + const targetNodeID = this.dll[dllIdx].nodeID; + + // Highlight Hash Map row & DLL node and draw active focused lookup pointer + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, HIGHLIGHT_COLOR); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, HIGHLIGHT_COLOR); + this.cmd(act.setBackgroundColor, targetNodeID, HIGHLIGHT_COLOR); + this.cmd( + act.connect, + mapEntry.ptrCellID, + targetNodeID, + MAP_LINK_COLOR, + 0.0, + true, + '', + 0, + ); + + this.highlight(2, 0, 'put'); + this.setStatus(STATUS.PUT_HIT(key, value)); + this.cmd(act.step); + + // Update value + this.dll[dllIdx].value = value; + this.cmd(act.setText, this.dll[dllIdx].valLabelID, value); + this.highlight(3, 0, 'put'); + this.cmd(act.step); + + // Move to head (MRU) + this.highlight(4, 0, 'put'); + this.setStatus(STATUS.MOVE_TO_MRU(key)); + this.moveToHead(dllIdx); + this.cmd(act.step); + + // Reset colors & remove active pointer arrow + this.cmd(act.disconnect, mapEntry.ptrCellID, targetNodeID); + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, KEY_CELL_BG); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, PTR_CELL_BG); + this.cmd(act.setBackgroundColor, targetNodeID, WHITE); + } else { + // ── Key is new ─────────────────────────────────────────────────── + this.highlight(5, 0, 'put'); + this.cmd(act.step); + + // Evict LRU if at capacity + if (this.dll.length >= this.capacity) { + this.highlight(9, 0, 'put'); + this.cmd(act.step); + + const tail = this.dll[this.dll.length - 1]; + const tailMap = this.map[tail.key]; + + this.cmd(act.setBackgroundColor, tail.nodeID, EVICT_COLOR); + if (tailMap) { + this.cmd(act.setBackgroundColor, tailMap.keyCellID, EVICT_COLOR); + this.cmd(act.setBackgroundColor, tailMap.ptrCellID, EVICT_COLOR); + this.cmd( + act.connect, + tailMap.ptrCellID, + tail.nodeID, + EVICT_COLOR, + 0.0, + true, + '', + 0, + ); + } + this.setStatus(STATUS.PUT_EVICT(this.capacity, tail.key)); + this.highlight(10, 0, 'put'); + this.cmd(act.step); + + if (tailMap) { + this.cmd(act.disconnect, tailMap.ptrCellID, tail.nodeID); + } + this.evictLRU(); + this.highlight(11, 0, 'put'); + this.highlight(12, 0, 'put'); + this.cmd(act.step); + } + + this.highlight(6, 0, 'put'); + this.setStatus(STATUS.PUT_INSERT(key, value)); + this.insertHead(key, value); + this.highlight(7, 0, 'put'); + this.highlight(8, 0, 'put'); + this.cmd(act.step); + } + + this.unhighlightAll('put'); + this.redrawMapAndDLL(); + this.setStatus(STATUS.PUT_COMPLETE(key, value)); + return this.commands; + } + + // ── Animation: get ──────────────────────────────────────────────────────── + + animateGet(key) { + this.commands = []; + this.setStatus(''); + this.highlight(0, 0, 'get'); + this.cmd(act.step); + + if (!(key in this.map)) { + this.highlight(1, 0, 'get'); + this.setStatus(STATUS.GET_MISS(key)); + this.cmd(act.step); + this.unhighlightAll('get'); + return this.commands; + } + + this.highlight(2, 0, 'get'); + const mapEntry = this.map[key]; + const dllIdx = this.dll.findIndex(n => n.key === key); + const targetNodeID = this.dll[dllIdx].nodeID; + + // Flash Hash Map row & DLL node in green with active focused lookup arrow + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, FOUND_COLOR); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, FOUND_COLOR); + this.cmd(act.setBackgroundColor, targetNodeID, FOUND_COLOR); + this.cmd(act.connect, mapEntry.ptrCellID, targetNodeID, MAP_LINK_COLOR, 0.0, true, '', 0); + + this.setStatus(STATUS.GET_HIT(key)); + this.cmd(act.step); + + this.highlight(3, 0, 'get'); + this.setStatus(STATUS.MOVE_TO_MRU(key)); + this.moveToHead(dllIdx); + this.cmd(act.step); + + // Disconnect active pointer and reset colors + this.cmd(act.disconnect, mapEntry.ptrCellID, targetNodeID); + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, KEY_CELL_BG); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, PTR_CELL_BG); + this.cmd(act.setBackgroundColor, targetNodeID, WHITE); + + this.unhighlightAll('get'); + this.redrawMapAndDLL(); + this.setStatus(STATUS.GET_COMPLETE(key, this.dll[0].value)); + return this.commands; + } + + // ── Animation: delete ───────────────────────────────────────────────────── + + animateDelete(key) { + this.commands = []; + this.setStatus(''); + this.highlight(0, 0, 'delete'); + this.cmd(act.step); + + if (!(key in this.map)) { + this.highlight(1, 0, 'delete'); + this.setStatus(STATUS.DEL_MISS(key)); + this.cmd(act.step); + this.unhighlightAll('delete'); + return this.commands; + } + + this.highlight(2, 0, 'delete'); + const mapEntry = this.map[key]; + const dllIdx = this.dll.findIndex(n => n.key === key); + const targetNodeID = this.dll[dllIdx].nodeID; + + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, EVICT_COLOR); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, EVICT_COLOR); + this.cmd(act.setBackgroundColor, targetNodeID, EVICT_COLOR); + this.cmd(act.connect, mapEntry.ptrCellID, targetNodeID, EVICT_COLOR, 0.0, true, '', 0); + + this.setStatus(STATUS.DEL_FOUND(key)); + this.cmd(act.step); + + this.cmd(act.disconnect, mapEntry.ptrCellID, targetNodeID); + this.highlight(3, 0, 'delete'); + this.removeEntry(key); + this.cmd(act.step); + + this.unhighlightAll('delete'); + this.redrawMapAndDLL(); + this.setStatus(STATUS.DEL_COMPLETE(key)); + return this.commands; + } + + // ── Core internal mutations ─────────────────────────────────────────────── + + insertHead(key, value) { + const nodeID = this.nextIndex++; + const valLabelID = this.nextIndex++; + const keyCellID = this.nextIndex++; + const ptrCellID = this.nextIndex++; + + const dllX = this.dllNodeX(0); + const dropY = DLL_START_Y - 50; + + // 1. Create DLL node (authentic 3-compartment doubly linked list node) + this.cmd(act.createDoublyLinkedListNode, nodeID, key, DLL_NODE_W, DLL_NODE_H, dllX, dropY); + this.cmd(act.setPrevNull, nodeID, 1); + this.cmd(act.setNextNull, nodeID, 1); + this.cmd(act.createLabel, valLabelID, value, dllX, dropY + VAL_OFFSET_Y, 1); + this.cmd(act.setForegroundColor, valLabelID, '#444444'); + + // 2. Create Hash Map entry row + const rowIdx = this.mapKeys.length; + const mapY = this.mapRowY(rowIdx); + + this.cmd(act.createRectangle, keyCellID, key, MAP_KEY_W, MAP_ROW_H, MAP_COL_KEY_X, mapY); + this.cmd(act.setBackgroundColor, keyCellID, KEY_CELL_BG); + this.cmd(act.setForegroundColor, keyCellID, CELL_BORDER); + + this.cmd(act.createRectangle, ptrCellID, '•', MAP_REF_W, MAP_ROW_H, MAP_COL_REF_X, mapY); + this.cmd(act.setBackgroundColor, ptrCellID, PTR_CELL_BG); + this.cmd(act.setForegroundColor, ptrCellID, CELL_BORDER); + this.cmd(act.setTextColor, ptrCellID, MAP_LINK_COLOR); + + // 3. Connect active pointer during insertion step + this.cmd(act.connect, ptrCellID, nodeID, MAP_LINK_COLOR, 0.0, true, '', 0); + + // Flash gold to emphasize insertion + this.cmd(act.setBackgroundColor, nodeID, HIGHLIGHT_COLOR); + this.cmd(act.setBackgroundColor, keyCellID, HIGHLIGHT_COLOR); + this.cmd(act.setBackgroundColor, ptrCellID, HIGHLIGHT_COLOR); + this.cmd(act.step); + + // Disconnect active pointer and reset colors back to standard + this.cmd(act.disconnect, ptrCellID, nodeID); + this.cmd(act.setBackgroundColor, nodeID, WHITE); + this.cmd(act.setBackgroundColor, keyCellID, KEY_CELL_BG); + this.cmd(act.setBackgroundColor, ptrCellID, PTR_CELL_BG); + + // Record in state + this.map[key] = { keyCellID, ptrCellID, nodeID, valLabelID }; + this.mapKeys.push(key); + this.dll.unshift({ key, value, nodeID, valLabelID }); + } + + moveToHead(srcIdx) { + if (srcIdx === 0) return; + const node = this.dll.splice(srcIdx, 1)[0]; + this.dll.unshift(node); + } + + evictLRU() { + if (this.dll.length === 0) return; + const tail = this.dll[this.dll.length - 1]; + this.removeEntry(tail.key); + } + + removeEntry(key) { + const mapEntry = this.map[key]; + const dllIdx = this.dll.findIndex(n => n.key === key); + + if (mapEntry) { + this.cmd(act.delete, mapEntry.keyCellID); + this.cmd(act.delete, mapEntry.ptrCellID); + delete this.map[key]; + this.mapKeys = this.mapKeys.filter(k => k !== key); + } + + if (dllIdx !== -1) { + const node = this.dll[dllIdx]; + this.cmd(act.delete, node.nodeID); + this.cmd(act.delete, node.valLabelID); + this.dll.splice(dllIdx, 1); + } + } + + // ── Redraw & layout synchronization ──────────────────────────────────────── + + redrawMapAndDLL() { + // 1. Clear previous DLL connections + for (const conn of this.dllConnections) { + this.cmd(act.disconnect, conn.from, conn.to); + } + this.dllConnections = []; + + // 2. Reposition DLL nodes and value labels + for (let i = 0; i < this.dll.length; i++) { + const x = this.dllNodeX(i); + this.cmd(act.move, this.dll[i].nodeID, x, DLL_START_Y); + this.cmd(act.move, this.dll[i].valLabelID, x, DLL_START_Y + VAL_OFFSET_Y); + } + + // 3. Set DLL null pointers and bi-directional edges + for (let i = 0; i < this.dll.length; i++) { + this.cmd(act.setPrevNull, this.dll[i].nodeID, i === 0 ? 1 : 0); + this.cmd(act.setNextNull, this.dll[i].nodeID, i === this.dll.length - 1 ? 1 : 0); + } + for (let i = 0; i < this.dll.length - 1; i++) { + const from = this.dll[i].nodeID; + const to = this.dll[i + 1].nodeID; + this.cmd(act.connectNext, from, to); + this.dllConnections.push({ from, to }); + this.cmd(act.connectPrev, to, from); + this.dllConnections.push({ from: to, to: from }); + } + + // 4. Reposition Hash Map rows cleanly + for (let i = 0; i < this.mapKeys.length; i++) { + const k = this.mapKeys[i]; + const entry = this.map[k]; + if (entry) { + const rowY = this.mapRowY(i); + this.cmd(act.move, entry.keyCellID, MAP_COL_KEY_X, rowY); + this.cmd(act.move, entry.ptrCellID, MAP_COL_REF_X, rowY); + } + } + + // 5. Update MRU / LRU Badges with comfortable vertical clearance above nodes + if (this.dll.length === 0) { + // Hide badges offscreen + this.cmd(act.move, this.mruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.mruTextID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); + } else if (this.dll.length === 1) { + // Capacity = 1 / Size = 1 edge case: single combined badge + const x = this.dllNodeX(0); + this.cmd(act.setText, this.mruTextID, 'MRU / LRU'); + this.cmd(act.setWidth, this.mruBadgeID, 72); + this.cmd(act.setBackgroundColor, this.mruBadgeID, COMBO_BADGE_COLOR); + this.cmd(act.setForegroundColor, this.mruBadgeID, COMBO_BADGE_COLOR); + this.cmd(act.move, this.mruBadgeID, x, BADGE_Y); + this.cmd(act.move, this.mruTextID, x, BADGE_Y); + + // Hide secondary LRU badge + this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); + } else { + // Size > 1: separate MRU and LRU badges + const mruX = this.dllNodeX(0); + const lruX = this.dllNodeX(this.dll.length - 1); + + this.cmd(act.setText, this.mruTextID, 'MRU'); + this.cmd(act.setWidth, this.mruBadgeID, BADGE_W); + this.cmd(act.setBackgroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.setForegroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.move, this.mruBadgeID, mruX, BADGE_Y); + this.cmd(act.move, this.mruTextID, mruX, BADGE_Y); + + this.cmd(act.setText, this.lruTextID, 'LRU'); + this.cmd(act.setWidth, this.lruBadgeID, BADGE_W); + this.cmd(act.setBackgroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.setForegroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.move, this.lruBadgeID, lruX, BADGE_Y); + this.cmd(act.move, this.lruTextID, lruX, BADGE_Y); + } + + // 6. Update capacity counter + this.cmd(act.setText, this.capLabelID, this.capText()); + this.cmd(act.step); + } + + unhighlightAll(op) { + const lines = (this.pseudocode && this.pseudocode[op] && this.pseudocode[op].code) || []; + for (let i = 0; i < lines.length; i++) { + this.unhighlight(i, 0, op); + } + } + + // ── Clear / reset ────────────────────────────────────────────────────────── + + clearAll() { + this.commands = []; + this.setStatus(''); + + this.dllConnections = []; + + // Destroy all DLL nodes and their value labels + for (let i = 0; i < this.dll.length; i++) { + this.cmd(act.delete, this.dll[i].nodeID); + this.cmd(act.delete, this.dll[i].valLabelID); + } + this.dll = []; + + // Destroy all Hash Map rows + for (const k of this.mapKeys) { + const entry = this.map[k]; + if (entry) { + this.cmd(act.delete, entry.keyCellID); + this.cmd(act.delete, entry.ptrCellID); + } + } + this.map = Object.create(null); + this.mapKeys = []; + this.inspectedKey = null; + + // Hide badges + this.cmd(act.move, this.mruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.mruTextID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); + + // Reset capacity counter + this.cmd(act.setText, this.capLabelID, this.capText()); + + return this.commands; + } +} diff --git a/src/algo/__tests__/LRU.test.js b/src/algo/__tests__/LRU.test.js new file mode 100644 index 0000000..413e3ea --- /dev/null +++ b/src/algo/__tests__/LRU.test.js @@ -0,0 +1,339 @@ +import LRU, { STATUS } from '../LRU'; +import pseudocodeText from '../../pseudocode.json'; +import timeComplexities from '../../time_complexities.json'; + +describe('LRU Cache metadata', () => { + test('LRU pseudocode is defined for put, get, and delete', () => { + expect(pseudocodeText.LRU).toBeDefined(); + expect(pseudocodeText.LRU.put).toBeDefined(); + expect(pseudocodeText.LRU.get).toBeDefined(); + expect(pseudocodeText.LRU.delete).toBeDefined(); + + expect(pseudocodeText.LRU.put.code.length).toBeGreaterThan(0); + expect(pseudocodeText.LRU.get.code.length).toBeGreaterThan(0); + expect(pseudocodeText.LRU.delete.code.length).toBeGreaterThan(0); + }); + + test('LRU time complexities are properly defined', () => { + expect(timeComplexities.LRU).toBeDefined(); + expect(timeComplexities.LRU['LRU Cache']).toBeDefined(); + const ops = timeComplexities.LRU['LRU Cache']; + expect(ops['put (no eviction)'].big_o).toBe('O(1)'); + expect(ops['put (with eviction)'].big_o).toBe('O(1)'); + expect(ops['get'].big_o).toBe('O(1)'); + expect(ops['delete'].big_o).toBe('O(1)'); + }); + + test('STATUS dictionary formats expected status messages', () => { + expect(STATUS.NEED_KEY_VALUE).toBe('Please enter both a key and a value.'); + expect(STATUS.FORBIDDEN_KEY('__proto__')).toContain('__proto__'); + expect(STATUS.INVALID_CAPACITY(10)).toContain('between 1 and 10'); + expect(STATUS.PUT_HIT('k1', 'v1')).toContain('Cache Hit on key "k1"'); + expect(STATUS.PUT_EVICT(5, 'k_old')).toContain('evicting LRU key "k_old"'); + expect(STATUS.GET_MISS('missing')).toContain('Cache Miss'); + expect(STATUS.GET_HIT('k1')).toContain('Cache Hit'); + expect(STATUS.DEL_COMPLETE('k1')).toContain('delete("k1") complete'); + expect(STATUS.INSPECT('k1', 'v1', 'index 0 (MRU)')).toContain('index 0 (MRU)'); + }); +}); + +describe('LRU Visualization Class (LRU.js)', () => { + let mockAm; + let lru; + + beforeEach(() => { + document.body.innerHTML = ` +
+
+ + `; + + mockAm = { + addListener: jest.fn(), + startNewAnimation: jest.fn(), + setAnimationDelay: jest.fn(), + skipForward: jest.fn(), + clearHistory: jest.fn(), + currentlyAnimating: false, + }; + + lru = new LRU(mockAm, 1000, 600); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + test('initializes default state, controls, and canvas structures', () => { + expect(lru.capacity).toBe(5); + expect(lru.map).toBeDefined(); + expect(Object.keys(lru.map).length).toBe(0); + expect(lru.dll).toEqual([]); + expect(lru.controls.length).toBeGreaterThan(0); + expect(lru.putKeyField).toBeDefined(); + expect(lru.putValField).toBeDefined(); + expect(lru.putButton).toBeDefined(); + expect(lru.getKeyField).toBeDefined(); + expect(lru.getButton).toBeDefined(); + expect(lru.deleteKeyField).toBeDefined(); + expect(lru.deleteButton).toBeDefined(); + }); + + test('animatePut inserts into cache map and doubly linked list at MRU', () => { + lru.animatePut('k1', 'v1'); + + expect(lru.map['k1']).toBeDefined(); + expect(lru.dll.length).toBe(1); + expect(lru.dll[0].key).toBe('k1'); + expect(lru.dll[0].value).toBe('v1'); + + // Insert second element + lru.animatePut('k2', 'v2'); + expect(lru.dll.length).toBe(2); + expect(lru.dll[0].key).toBe('k2'); // MRU + expect(lru.dll[1].key).toBe('k1'); // LRU + }); + + test('animateGet on existing key moves it to MRU (head of list)', () => { + lru.animatePut('k1', 'v1'); + lru.animatePut('k2', 'v2'); + lru.animatePut('k3', 'v3'); + // Initial order: k3 (MRU), k2, k1 (LRU) + expect(lru.dll.map(n => n.key)).toEqual(['k3', 'k2', 'k1']); + + const cmds = lru.animateGet('k1'); + expect(cmds.length).toBeGreaterThan(0); + // After get(k1), k1 must become MRU + expect(lru.dll.map(n => n.key)).toEqual(['k1', 'k3', 'k2']); + }); + + test('animateGet on nonexistent key leaves list unchanged', () => { + lru.animatePut('k1', 'v1'); + const cmds = lru.animateGet('nonexistent'); + expect(cmds.length).toBeGreaterThan(0); + expect(lru.dll.length).toBe(1); + expect(lru.dll[0].key).toBe('k1'); + }); + + test('animatePut updating an existing key updates value and promotes to MRU without increasing size', () => { + lru.animatePut('k1', 'v1'); + lru.animatePut('k2', 'v2'); + expect(lru.dll.length).toBe(2); + + lru.animatePut('k1', 'v1_updated'); + expect(lru.dll.length).toBe(2); + expect(lru.dll[0].key).toBe('k1'); + expect(lru.dll[0].value).toBe('v1_updated'); + expect(lru.dll[1].key).toBe('k2'); + }); + + test('evicts the LRU tail node when capacity is exceeded', () => { + lru.capacity = 2; + lru.animatePut('1', 'one'); + lru.animatePut('2', 'two'); + expect(lru.dll.map(n => n.key)).toEqual(['2', '1']); + + // Access '1' to make '2' the LRU node + lru.animateGet('1'); + expect(lru.dll.map(n => n.key)).toEqual(['1', '2']); + + // Put '3' -> evicts '2' + lru.animatePut('3', 'three'); + expect(lru.map['2']).toBeUndefined(); + expect(lru.map['1']).toBeDefined(); + expect(lru.map['3']).toBeDefined(); + expect(lru.dll.map(n => n.key)).toEqual(['3', '1']); + }); + + test('animateDelete removes element from cache map and dll', () => { + lru.animatePut('1', 'a'); + lru.animatePut('2', 'b'); + expect(lru.dll.length).toBe(2); + + lru.animateDelete('1'); + expect(lru.map['1']).toBeUndefined(); + expect(lru.dll.length).toBe(1); + expect(lru.dll[0].key).toBe('2'); + + // Deleting nonexistent key causes no errors + const cmds = lru.animateDelete('nonexistent'); + expect(cmds.length).toBeGreaterThan(0); + expect(lru.dll.length).toBe(1); + }); + + test('handles inspection mode and clearInspection', () => { + lru.animatePut('k1', 'v1'); + lru.animatePut('k2', 'v2'); + + lru.inspectKey('k1'); + expect(lru.inspectedKey).toBe('k1'); + + lru.clearInspection(); + expect(lru.inspectedKey).toBeNull(); + }); + + test('putCallback processes valid input and clears fields', () => { + lru.putKeyField.value = 'userKey'; + lru.putValField.value = 'userVal'; + lru.putCallback(); + + expect(lru.map['userKey']).toBeDefined(); + expect(lru.dll[0].key).toBe('userKey'); + expect(lru.dll[0].value).toBe('userVal'); + expect(lru.putKeyField.value).toBe(''); + expect(lru.putValField.value).toBe(''); + }); + + test('putCallback with empty inputs shakes button and shows error', () => { + lru.putKeyField.value = ''; + lru.putValField.value = ''; + lru.putCallback(); + + expect(lru.putButton.classList.contains('shake')).toBe(true); + expect(mockAm.startNewAnimation).toHaveBeenCalled(); + }); + + test('getCallback processes valid key, promotes to MRU, and clears field', () => { + lru.animatePut('g1', 'v1'); + lru.animatePut('g2', 'v2'); + + lru.getKeyField.value = 'g1'; + lru.getCallback(); + + expect(lru.dll[0].key).toBe('g1'); + expect(lru.getKeyField.value).toBe(''); + }); + + test('getCallback with empty key shakes button and shows error', () => { + lru.getKeyField.value = ''; + lru.getCallback(); + + expect(lru.getButton.classList.contains('shake')).toBe(true); + }); + + test('deleteCallback processes valid key and clears field', () => { + lru.animatePut('d1', 'v1'); + lru.deleteKeyField.value = 'd1'; + lru.deleteCallback(); + + expect(lru.map['d1']).toBeUndefined(); + expect(lru.dll.length).toBe(0); + expect(lru.deleteKeyField.value).toBe(''); + }); + + test('deleteCallback with empty key shakes button and shows error', () => { + lru.deleteKeyField.value = ''; + lru.deleteCallback(); + + expect(lru.deleteButton.classList.contains('shake')).toBe(true); + }); + + test('clearCallback empties cache while preserving capacity setting', () => { + lru.capacity = 3; + lru.animatePut('c1', 'v1'); + lru.animatePut('c2', 'v2'); + expect(lru.dll.length).toBe(2); + + lru.clearCallback(); + + expect(lru.capacity).toBe(3); + expect(lru.dll.length).toBe(0); + expect(Object.keys(lru.map).length).toBe(0); + expect(lru.inspectedKey).toBeNull(); + }); + + test('canvas mousemove updates cursor style based on hit testing', () => { + lru.animatePut('m1', 'v1'); + lru.canvas.getBoundingClientRect = () => ({ + left: 0, + top: 0, + width: 1000, + height: 600, + }); + + // Hover directly over row 0 (MAP_START_Y = 130) + lru.handleCanvasMouseMove({ clientX: 48, clientY: 130 }); + expect(lru.canvas.style.cursor).toBe('pointer'); + + // Hover outside row 0 + lru.handleCanvasMouseMove({ clientX: 500, clientY: 500 }); + expect(lru.canvas.style.cursor).toBe('default'); + }); + + test('canvas click toggles inspection and handles outside click', () => { + lru.animatePut('c1', 'v1'); + lru.canvas.getBoundingClientRect = () => ({ + left: 0, + top: 0, + width: 1000, + height: 600, + }); + + // Click on row 0 -> inspects c1 + lru.handleCanvasClick({ clientX: 48, clientY: 130 }); + expect(lru.inspectedKey).toBe('c1'); + + // Click same row again -> dismisses inspection (toggle off) + lru.handleCanvasClick({ clientX: 48, clientY: 130 }); + expect(lru.inspectedKey).toBeNull(); + + // Click on row 0 again -> inspects + lru.handleCanvasClick({ clientX: 48, clientY: 130 }); + expect(lru.inspectedKey).toBe('c1'); + + // Click outside -> dismisses inspection + lru.handleCanvasClick({ clientX: 500, clientY: 500 }); + expect(lru.inspectedKey).toBeNull(); + + // Clicking while animating is ignored + lru.animationManager.currentlyAnimating = true; + lru.handleCanvasClick({ clientX: 48, clientY: 130 }); + expect(lru.inspectedKey).toBeNull(); + lru.animationManager.currentlyAnimating = false; + }); + + test('starting any operation automatically dismisses active inspection', () => { + lru.animatePut('x1', 'v1'); + lru.inspectKey('x1'); + expect(lru.inspectedKey).toBe('x1'); + + // Starting a putCallback dismisses inspection + lru.putKeyField.value = 'x2'; + lru.putValField.value = 'v2'; + lru.putCallback(); + expect(lru.inspectedKey).toBeNull(); + }); + + test('rejects invalid capacity inputs during restartCallback', () => { + lru.capacity = 3; + + // Test 0 + lru.capacityField.value = '0'; + lru.restartCallback(); + expect(lru.capacity).toBe(3); // unchanged + + // Test negative + lru.capacityField.value = '-5'; + lru.restartCallback(); + expect(lru.capacity).toBe(3); + + // Test > 10 + lru.capacityField.value = '11'; + lru.restartCallback(); + expect(lru.capacity).toBe(3); + + // Test valid capacity + lru.capacityField.value = '7'; + lru.restartCallback(); + expect(lru.capacity).toBe(7); + }); + + test('rejects forbidden prototype keys in putCallback', () => { + lru.putKeyField.value = '__proto__'; + lru.putValField.value = 'danger'; + lru.putCallback(); + + expect(lru.map['__proto__']).toBeUndefined(); + expect(lru.dll.length).toBe(0); + }); +}); diff --git a/src/algo/index.js b/src/algo/index.js index 475b55a..f89c9f6 100644 --- a/src/algo/index.js +++ b/src/algo/index.js @@ -14,6 +14,7 @@ export { default as LVA } from './LVA'; export { default as BogoSort } from './BogoSort'; export { default as NonLinearProbing } from './NonLinearProbing'; export { default as LCS } from './LCS'; +export { default as LRU } from './LRU'; export { default as DequeArray } from './DequeArray'; export { default as DequeLL } from './DequeLL'; export { default as Dijkstras } from './Dijkstras'; diff --git a/src/pseudocode.json b/src/pseudocode.json index ab0046e..f8ccfae 100644 --- a/src/pseudocode.json +++ b/src/pseudocode.json @@ -1,4 +1,74 @@ { + "LRU": { + "put": { + "code": [ + ["procedure put(key, value):"], + [" if key in map:"], + [" node ← map[key]"], + [" update node.value ← value"], + [" move node to head (MRU)"], + [" else:"], + [" create new node(key, value)"], + [" insert node at head"], + [" map[key] ← node"], + [" if size > capacity:"], + [" evict ← tail (LRU node)"], + [" remove evict from list"], + [" delete map[evict.key]"], + ["end procedure"] + ], + "english": [ + ["procedure put(key, value):"], + [" if key already exists in cache:"], + [" look up the node in the cache"], + [" update its value"], + [" move it to the front (MRU)"], + [" otherwise:"], + [" create a new cache entry"], + [" insert at front of list"], + [" record it in the cache"], + [" if over capacity:"], + [" identify the tail (LRU entry)"], + [" remove it from the list"], + [" remove it from the cache"], + ["end procedure"] + ] + }, + "get": { + "code": [ + ["procedure get(key):"], + [" if key not in map: return -1"], + [" node ← map[key]"], + [" move node to head (MRU)"], + [" return node.value"], + ["end procedure"] + ], + "english": [ + ["procedure get(key):"], + [" if key not in cache: return -1"], + [" look up the node in the cache"], + [" move the node to the front (MRU)"], + [" return the node's value"], + ["end procedure"] + ] + }, + "delete": { + "code": [ + ["procedure delete(key):"], + [" if key not in map: return"], + [" node ← map[key]"], + [" remove node from list; delete map[key]"], + ["end procedure"] + ], + "english": [ + ["procedure delete(key):"], + [" if key not in cache: return"], + [" find the node in the cache"], + [" remove from list and cache"], + ["end procedure"] + ] + } + }, "ArrayList": { "addFB": { "code": [ diff --git a/src/time_complexities.json b/src/time_complexities.json index 5608b26..f172863 100644 --- a/src/time_complexities.json +++ b/src/time_complexities.json @@ -1560,5 +1560,25 @@ "explanation": "Each letter in the first string is compared to each letter of the second string." } } + }, + "LRU": { + "LRU Cache": { + "put (no eviction)": { + "big_o": "O(1)", + "explanation": "HashMap lookup + doubly-linked list head insertion are both O(1)." + }, + "put (with eviction)": { + "big_o": "O(1)", + "explanation": "Tail removal from the DLL + HashMap delete are both O(1)." + }, + "get": { + "big_o": "O(1)", + "explanation": "HashMap lookup finds the node in O(1); moving it to head in the DLL is also O(1)." + }, + "delete": { + "big_o": "O(1)", + "explanation": "HashMap lookup + DLL node removal are both O(1)." + } + } } } From acc3a2e40c1497c0fe7f4591ff1614294796be43 Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Sat, 26 Sep 2026 18:19:40 -0400 Subject: [PATCH 2/9] lru icon --- public/algo_buttons/LRU Cache.png | Bin 0 -> 18806 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/algo_buttons/LRU Cache.png diff --git a/public/algo_buttons/LRU Cache.png b/public/algo_buttons/LRU Cache.png new file mode 100644 index 0000000000000000000000000000000000000000..d28c9701664f6d21da0a016db2ce4885438c72af GIT binary patch literal 18806 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 literal 0 HcmV?d00001 From 3137957fdec76ea0c1bb5ccd31c5c071f2c542f4 Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Sat, 26 Sep 2026 18:32:56 -0400 Subject: [PATCH 3/9] LRU code cleanup --- src/algo/LRU.js | 270 +++++++++++++++++++++--------------------------- 1 file changed, 119 insertions(+), 151 deletions(-) diff --git a/src/algo/LRU.js b/src/algo/LRU.js index 5d2bf4e..b08b296 100644 --- a/src/algo/LRU.js +++ b/src/algo/LRU.js @@ -29,7 +29,7 @@ import Algorithm, { import { act } from '../anim/AnimationMain'; import pseudocodeText from '../pseudocode.json'; -// ─── Layout constants ──────────────────────────────────────────────────────── +// Layout Configurations const INFO_MSG_X = 25; const INFO_MSG_Y = 15; @@ -67,7 +67,7 @@ const BADGE_Y = DLL_START_Y - 50; // Off-screen position used to "hide" canvas objects we can't destroy const OFFSCREEN = -2000; -// ─── Colors ────────────────────────────────────────────────────────────────── +// Colors const MRU_COLOR = '#006400'; // dark green const LRU_COLOR = '#CC0000'; // crimson red @@ -81,7 +81,7 @@ const PTR_CELL_BG = '#E3F2FD'; const CELL_BORDER = '#B0BEC5'; const WHITE = '#FFFFFF'; -// ─── Limits ─────────────────────────────────────────────────────────────────── +// Limits for the simulation const MAX_CAPACITY = 10; const DEFAULT_CAPACITY = 5; @@ -91,7 +91,7 @@ const MAX_VAL_LENGTH = 4; // Keys that would cause prototype pollution const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); -// ─── Status Message Helpers ─────────────────────────────────────────────────── +// Status Message Helpers export const STATUS = { // Validation / Input errors @@ -123,7 +123,7 @@ export const STATUS = { `Inspecting: key "${key}" maps to DLL node "${key}" (value: "${val}") at ${pos}.`, }; -// ─── LRU class ─────────────────────────────────────────────────────────────── +// LRU Visualizer export default class LRU extends Algorithm { constructor(am, w, h) { @@ -136,7 +136,7 @@ export default class LRU extends Algorithm { this.setup(); } - // ── Control bar ──────────────────────────────────────────────────────────── + // Control bar addControls() { this.controls = []; @@ -240,21 +240,14 @@ export default class LRU extends Algorithm { this.controls.push(this.clearButton); } - // ── Setup ────────────────────────────────────────────────────────────────── - setup() { - // JS-side state this.capacity = DEFAULT_CAPACITY; - // O(1) lookup map: key → { keyCellID, ptrCellID, nodeID, valLabelID } this.map = Object.create(null); - // Active keys in the Hash Map this.mapKeys = []; - // Ordered MRU→LRU: [{ key, value, nodeID, valLabelID }] this.dll = []; - // Tracked DLL connections this.dllConnections = []; - // ── Permanent canvas objects ─────────────────────────────────────────── + // Canvas Headers this.infoLabelID = this.nextIndex++; this.cmd(act.createLabel, this.infoLabelID, '', INFO_MSG_X, INFO_MSG_Y, 0); @@ -334,7 +327,7 @@ export default class LRU extends Algorithm { this.nextIndex = this.resetIndex; } - // ── Interactive Click-to-Inspect ─────────────────────────────────────────── + // Interactive Click-to-Inspect getCanvasCoords(event) { if (!this.canvas) return { x: -1, y: -1 }; @@ -389,26 +382,14 @@ export default class LRU extends Algorithm { inspectKey(key) { this.clearInspection(); const mapEntry = this.map[key]; - const dllIdx = this.dll.findIndex(n => n.key === key); + const dllIdx = this.getDllIndex(key); if (!mapEntry || dllIdx === -1) return; const targetNode = this.dll[dllIdx]; this.inspectedKey = key; this.commands = []; - this.cmd(act.setBackgroundColor, mapEntry.keyCellID, PTR_CELL_BG); - this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, '#BBDEFB'); - this.cmd(act.setBackgroundColor, targetNode.nodeID, '#E1F5FE'); - this.cmd( - act.connect, - mapEntry.ptrCellID, - targetNode.nodeID, - MAP_LINK_COLOR, - 0.0, - true, - '', - 0, - ); + this.highlightEntryAndNode(key, PTR_CELL_BG, '#E1F5FE', MAP_LINK_COLOR); const posDesc = dllIdx === 0 @@ -426,26 +407,17 @@ export default class LRU extends Algorithm { clearInspection() { if (!this.inspectedKey) return; const key = this.inspectedKey; - const mapEntry = this.map[key]; - const dllIdx = this.dll.findIndex(n => n.key === key); this.inspectedKey = null; this.commands = []; - if (mapEntry) { - this.cmd(act.setBackgroundColor, mapEntry.keyCellID, KEY_CELL_BG); - this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, PTR_CELL_BG); - if (dllIdx !== -1) { - this.cmd(act.disconnect, mapEntry.ptrCellID, this.dll[dllIdx].nodeID); - this.cmd(act.setBackgroundColor, this.dll[dllIdx].nodeID, WHITE); - } - } + this.resetEntryAndNode(key); this.setStatus(''); this.animationManager.startNewAnimation(this.commands); this.animationManager.skipForward(); this.commands = []; } - // ── UI lifecycle ─────────────────────────────────────────────────────────── + // UI lifecycle enableUI() { for (let i = 0; i < this.controls.length; i++) { @@ -459,7 +431,7 @@ export default class LRU extends Algorithm { } } - // ── Helpers ──────────────────────────────────────────────────────────────── + // Helpers setStatus(text) { this.cmd(act.setText, this.infoLabelID, text); @@ -477,7 +449,87 @@ export default class LRU extends Algorithm { return MAP_START_Y + idx * MAP_ROW_GAP; } - // ── Callbacks ────────────────────────────────────────────────────────────── + getDllIndex(key) { + return this.dll.findIndex(n => n.key === key); + } + + highlightEntryAndNode( + key, + cellColor, + nodeColor = cellColor, + linkColor = MAP_LINK_COLOR, + keyCellColor = cellColor, + ) { + const mapEntry = this.map[key]; + const dllIdx = this.getDllIndex(key); + if (!mapEntry || dllIdx === -1) return; + + const targetNodeID = this.dll[dllIdx].nodeID; + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, keyCellColor); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, cellColor); + this.cmd(act.setBackgroundColor, targetNodeID, nodeColor); + this.cmd(act.connect, mapEntry.ptrCellID, targetNodeID, linkColor, 0.0, true, '', 0); + } + + resetEntryAndNode(key, disconnect = true) { + const mapEntry = this.map[key]; + const dllIdx = this.getDllIndex(key); + + if (mapEntry) { + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, KEY_CELL_BG); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, PTR_CELL_BG); + if (dllIdx !== -1) { + const targetNodeID = this.dll[dllIdx].nodeID; + if (disconnect) { + this.cmd(act.disconnect, mapEntry.ptrCellID, targetNodeID); + } + this.cmd(act.setBackgroundColor, targetNodeID, WHITE); + } + } + } + + updateRecencyBadges() { + if (this.dll.length === 0) { + // Hide badges offscreen + this.cmd(act.move, this.mruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.mruTextID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); + } else if (this.dll.length === 1) { + // Capacity = 1 / Size = 1 edge case: single combined badge + const x = this.dllNodeX(0); + this.cmd(act.setText, this.mruTextID, 'MRU / LRU'); + this.cmd(act.setWidth, this.mruBadgeID, 72); + this.cmd(act.setBackgroundColor, this.mruBadgeID, COMBO_BADGE_COLOR); + this.cmd(act.setForegroundColor, this.mruBadgeID, COMBO_BADGE_COLOR); + this.cmd(act.move, this.mruBadgeID, x, BADGE_Y); + this.cmd(act.move, this.mruTextID, x, BADGE_Y); + + // Hide secondary LRU badge + this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); + } else { + // Size > 1: separate MRU and LRU badges + const mruX = this.dllNodeX(0); + const lruX = this.dllNodeX(this.dll.length - 1); + + this.cmd(act.setText, this.mruTextID, 'MRU'); + this.cmd(act.setWidth, this.mruBadgeID, BADGE_W); + this.cmd(act.setBackgroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.setForegroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.move, this.mruBadgeID, mruX, BADGE_Y); + this.cmd(act.move, this.mruTextID, mruX, BADGE_Y); + + this.cmd(act.setText, this.lruTextID, 'LRU'); + this.cmd(act.setWidth, this.lruBadgeID, BADGE_W); + this.cmd(act.setBackgroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.setForegroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.move, this.lruBadgeID, lruX, BADGE_Y); + this.cmd(act.move, this.lruTextID, lruX, BADGE_Y); + } + } + + // Callbacks for the supported operations of LRU Cache putCallback() { this.clearInspection(); @@ -549,7 +601,7 @@ export default class LRU extends Algorithm { return this.commands; } - // ── Animation: put ──────────────────────────────────────────────────────── + // Animation for put animatePut(key, value) { this.commands = []; @@ -558,28 +610,13 @@ export default class LRU extends Algorithm { this.cmd(act.step); if (key in this.map) { - // ── Key exists: update in map and move DLL node to head (MRU) ─── this.highlight(1, 0, 'put'); this.cmd(act.step); - const mapEntry = this.map[key]; - const dllIdx = this.dll.findIndex(n => n.key === key); - const targetNodeID = this.dll[dllIdx].nodeID; + const dllIdx = this.getDllIndex(key); // Highlight Hash Map row & DLL node and draw active focused lookup pointer - this.cmd(act.setBackgroundColor, mapEntry.keyCellID, HIGHLIGHT_COLOR); - this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, HIGHLIGHT_COLOR); - this.cmd(act.setBackgroundColor, targetNodeID, HIGHLIGHT_COLOR); - this.cmd( - act.connect, - mapEntry.ptrCellID, - targetNodeID, - MAP_LINK_COLOR, - 0.0, - true, - '', - 0, - ); + this.highlightEntryAndNode(key, HIGHLIGHT_COLOR); this.highlight(2, 0, 'put'); this.setStatus(STATUS.PUT_HIT(key, value)); @@ -598,12 +635,9 @@ export default class LRU extends Algorithm { this.cmd(act.step); // Reset colors & remove active pointer arrow - this.cmd(act.disconnect, mapEntry.ptrCellID, targetNodeID); - this.cmd(act.setBackgroundColor, mapEntry.keyCellID, KEY_CELL_BG); - this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, PTR_CELL_BG); - this.cmd(act.setBackgroundColor, targetNodeID, WHITE); + this.resetEntryAndNode(key); } else { - // ── Key is new ─────────────────────────────────────────────────── + // Key is new this.highlight(5, 0, 'put'); this.cmd(act.step); @@ -613,27 +647,12 @@ export default class LRU extends Algorithm { this.cmd(act.step); const tail = this.dll[this.dll.length - 1]; - const tailMap = this.map[tail.key]; - - this.cmd(act.setBackgroundColor, tail.nodeID, EVICT_COLOR); - if (tailMap) { - this.cmd(act.setBackgroundColor, tailMap.keyCellID, EVICT_COLOR); - this.cmd(act.setBackgroundColor, tailMap.ptrCellID, EVICT_COLOR); - this.cmd( - act.connect, - tailMap.ptrCellID, - tail.nodeID, - EVICT_COLOR, - 0.0, - true, - '', - 0, - ); - } + this.highlightEntryAndNode(tail.key, EVICT_COLOR, EVICT_COLOR, EVICT_COLOR); this.setStatus(STATUS.PUT_EVICT(this.capacity, tail.key)); this.highlight(10, 0, 'put'); this.cmd(act.step); + const tailMap = this.map[tail.key]; if (tailMap) { this.cmd(act.disconnect, tailMap.ptrCellID, tail.nodeID); } @@ -657,7 +676,7 @@ export default class LRU extends Algorithm { return this.commands; } - // ── Animation: get ──────────────────────────────────────────────────────── + // Animation for get animateGet(key) { this.commands = []; @@ -674,15 +693,10 @@ export default class LRU extends Algorithm { } this.highlight(2, 0, 'get'); - const mapEntry = this.map[key]; - const dllIdx = this.dll.findIndex(n => n.key === key); - const targetNodeID = this.dll[dllIdx].nodeID; + const dllIdx = this.getDllIndex(key); // Flash Hash Map row & DLL node in green with active focused lookup arrow - this.cmd(act.setBackgroundColor, mapEntry.keyCellID, FOUND_COLOR); - this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, FOUND_COLOR); - this.cmd(act.setBackgroundColor, targetNodeID, FOUND_COLOR); - this.cmd(act.connect, mapEntry.ptrCellID, targetNodeID, MAP_LINK_COLOR, 0.0, true, '', 0); + this.highlightEntryAndNode(key, FOUND_COLOR); this.setStatus(STATUS.GET_HIT(key)); this.cmd(act.step); @@ -693,10 +707,7 @@ export default class LRU extends Algorithm { this.cmd(act.step); // Disconnect active pointer and reset colors - this.cmd(act.disconnect, mapEntry.ptrCellID, targetNodeID); - this.cmd(act.setBackgroundColor, mapEntry.keyCellID, KEY_CELL_BG); - this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, PTR_CELL_BG); - this.cmd(act.setBackgroundColor, targetNodeID, WHITE); + this.resetEntryAndNode(key); this.unhighlightAll('get'); this.redrawMapAndDLL(); @@ -704,7 +715,7 @@ export default class LRU extends Algorithm { return this.commands; } - // ── Animation: delete ───────────────────────────────────────────────────── + // Animation for delete animateDelete(key) { this.commands = []; @@ -721,19 +732,16 @@ export default class LRU extends Algorithm { } this.highlight(2, 0, 'delete'); - const mapEntry = this.map[key]; - const dllIdx = this.dll.findIndex(n => n.key === key); - const targetNodeID = this.dll[dllIdx].nodeID; - - this.cmd(act.setBackgroundColor, mapEntry.keyCellID, EVICT_COLOR); - this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, EVICT_COLOR); - this.cmd(act.setBackgroundColor, targetNodeID, EVICT_COLOR); - this.cmd(act.connect, mapEntry.ptrCellID, targetNodeID, EVICT_COLOR, 0.0, true, '', 0); + this.highlightEntryAndNode(key, EVICT_COLOR, EVICT_COLOR, EVICT_COLOR); this.setStatus(STATUS.DEL_FOUND(key)); this.cmd(act.step); - this.cmd(act.disconnect, mapEntry.ptrCellID, targetNodeID); + const mapEntry = this.map[key]; + const dllIdx = this.getDllIndex(key); + if (mapEntry && dllIdx !== -1) { + this.cmd(act.disconnect, mapEntry.ptrCellID, this.dll[dllIdx].nodeID); + } this.highlight(3, 0, 'delete'); this.removeEntry(key); this.cmd(act.step); @@ -744,7 +752,7 @@ export default class LRU extends Algorithm { return this.commands; } - // ── Core internal mutations ─────────────────────────────────────────────── + // Core internal mutations insertHead(key, value) { const nodeID = this.nextIndex++; @@ -810,7 +818,7 @@ export default class LRU extends Algorithm { removeEntry(key) { const mapEntry = this.map[key]; - const dllIdx = this.dll.findIndex(n => n.key === key); + const dllIdx = this.getDllIndex(key); if (mapEntry) { this.cmd(act.delete, mapEntry.keyCellID); @@ -827,7 +835,7 @@ export default class LRU extends Algorithm { } } - // ── Redraw & layout synchronization ──────────────────────────────────────── + // Redraw & layout synchronization redrawMapAndDLL() { // 1. Clear previous DLL connections @@ -868,45 +876,8 @@ export default class LRU extends Algorithm { } } - // 5. Update MRU / LRU Badges with comfortable vertical clearance above nodes - if (this.dll.length === 0) { - // Hide badges offscreen - this.cmd(act.move, this.mruBadgeID, OFFSCREEN, OFFSCREEN); - this.cmd(act.move, this.mruTextID, OFFSCREEN, OFFSCREEN); - this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); - this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); - } else if (this.dll.length === 1) { - // Capacity = 1 / Size = 1 edge case: single combined badge - const x = this.dllNodeX(0); - this.cmd(act.setText, this.mruTextID, 'MRU / LRU'); - this.cmd(act.setWidth, this.mruBadgeID, 72); - this.cmd(act.setBackgroundColor, this.mruBadgeID, COMBO_BADGE_COLOR); - this.cmd(act.setForegroundColor, this.mruBadgeID, COMBO_BADGE_COLOR); - this.cmd(act.move, this.mruBadgeID, x, BADGE_Y); - this.cmd(act.move, this.mruTextID, x, BADGE_Y); - - // Hide secondary LRU badge - this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); - this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); - } else { - // Size > 1: separate MRU and LRU badges - const mruX = this.dllNodeX(0); - const lruX = this.dllNodeX(this.dll.length - 1); - - this.cmd(act.setText, this.mruTextID, 'MRU'); - this.cmd(act.setWidth, this.mruBadgeID, BADGE_W); - this.cmd(act.setBackgroundColor, this.mruBadgeID, MRU_COLOR); - this.cmd(act.setForegroundColor, this.mruBadgeID, MRU_COLOR); - this.cmd(act.move, this.mruBadgeID, mruX, BADGE_Y); - this.cmd(act.move, this.mruTextID, mruX, BADGE_Y); - - this.cmd(act.setText, this.lruTextID, 'LRU'); - this.cmd(act.setWidth, this.lruBadgeID, BADGE_W); - this.cmd(act.setBackgroundColor, this.lruBadgeID, LRU_COLOR); - this.cmd(act.setForegroundColor, this.lruBadgeID, LRU_COLOR); - this.cmd(act.move, this.lruBadgeID, lruX, BADGE_Y); - this.cmd(act.move, this.lruTextID, lruX, BADGE_Y); - } + // 5. Update MRU / LRU Badges + this.updateRecencyBadges(); // 6. Update capacity counter this.cmd(act.setText, this.capLabelID, this.capText()); @@ -920,7 +891,7 @@ export default class LRU extends Algorithm { } } - // ── Clear / reset ────────────────────────────────────────────────────────── + // Clear / reset clearAll() { this.commands = []; @@ -948,10 +919,7 @@ export default class LRU extends Algorithm { this.inspectedKey = null; // Hide badges - this.cmd(act.move, this.mruBadgeID, OFFSCREEN, OFFSCREEN); - this.cmd(act.move, this.mruTextID, OFFSCREEN, OFFSCREEN); - this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); - this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); + this.updateRecencyBadges(); // Reset capacity counter this.cmd(act.setText, this.capLabelID, this.capText()); From c63300b4cb6e3815e67b4ad772eeb9acc679d420 Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Sat, 26 Sep 2026 18:37:26 -0400 Subject: [PATCH 4/9] more refactoring --- src/algo/LRU.js | 117 +++++++++++++++++------------------------------- 1 file changed, 41 insertions(+), 76 deletions(-) diff --git a/src/algo/LRU.js b/src/algo/LRU.js index b08b296..167cdde 100644 --- a/src/algo/LRU.js +++ b/src/algo/LRU.js @@ -138,6 +138,21 @@ export default class LRU extends Algorithm { // Control bar + addInputField(parentGroup, size, callback, isIntOnly = false, initialVal = '') { + const field = addControlToAlgorithmBar('Text', initialVal, parentGroup); + field.size = size; + field.onkeydown = this.returnSubmit(field, callback.bind(this), size, isIntOnly); + this.controls.push(field); + return field; + } + + addButton(label, callback, parentGroup) { + const button = addControlToAlgorithmBar('Button', label, parentGroup); + button.onclick = callback.bind(this); + this.controls.push(button); + return button; + } + addControls() { this.controls = []; @@ -147,68 +162,28 @@ export default class LRU extends Algorithm { const putBot = addGroupToAlgorithmBar(true, putVG); addLabelToAlgorithmBar('Key:', putTop); - this.putKeyField = addControlToAlgorithmBar('Text', '', putTop); - this.putKeyField.size = MAX_KEY_LENGTH; - this.putKeyField.onkeydown = this.returnSubmit( - this.putKeyField, - this.putCallback.bind(this), - MAX_KEY_LENGTH, - false, - ); - this.controls.push(this.putKeyField); + this.putKeyField = this.addInputField(putTop, MAX_KEY_LENGTH, this.putCallback); addLabelToAlgorithmBar('Value:', putBot); - this.putValField = addControlToAlgorithmBar('Text', '', putBot); - this.putValField.size = MAX_VAL_LENGTH; - this.putValField.onkeydown = this.returnSubmit( - this.putValField, - this.putCallback.bind(this), - MAX_VAL_LENGTH, - false, - ); - this.controls.push(this.putValField); + this.putValField = this.addInputField(putBot, MAX_VAL_LENGTH, this.putCallback); - this.putButton = addControlToAlgorithmBar('Button', 'Put'); - this.putButton.onclick = this.putCallback.bind(this); - this.controls.push(this.putButton); + this.putButton = this.addButton('Put', this.putCallback); addDivisorToAlgorithmBar(); - // Get + // Get: Key const getG = addGroupToAlgorithmBar(true); addLabelToAlgorithmBar('Key:', getG); - this.getKeyField = addControlToAlgorithmBar('Text', '', getG); - this.getKeyField.size = MAX_KEY_LENGTH; - this.getKeyField.onkeydown = this.returnSubmit( - this.getKeyField, - this.getCallback.bind(this), - MAX_KEY_LENGTH, - false, - ); - this.controls.push(this.getKeyField); - - this.getButton = addControlToAlgorithmBar('Button', 'Get'); - this.getButton.onclick = this.getCallback.bind(this); - this.controls.push(this.getButton); + this.getKeyField = this.addInputField(getG, MAX_KEY_LENGTH, this.getCallback); + this.getButton = this.addButton('Get', this.getCallback); addDivisorToAlgorithmBar(); - // Delete + // Delete: Key const delG = addGroupToAlgorithmBar(true); addLabelToAlgorithmBar('Key:', delG); - this.deleteKeyField = addControlToAlgorithmBar('Text', '', delG); - this.deleteKeyField.size = MAX_KEY_LENGTH; - this.deleteKeyField.onkeydown = this.returnSubmit( - this.deleteKeyField, - this.deleteCallback.bind(this), - MAX_KEY_LENGTH, - false, - ); - this.controls.push(this.deleteKeyField); - - this.deleteButton = addControlToAlgorithmBar('Button', 'Delete'); - this.deleteButton.onclick = this.deleteCallback.bind(this); - this.controls.push(this.deleteButton); + this.deleteKeyField = this.addInputField(delG, MAX_KEY_LENGTH, this.deleteCallback); + this.deleteButton = this.addButton('Delete', this.deleteCallback); addDivisorToAlgorithmBar(); @@ -218,26 +193,19 @@ export default class LRU extends Algorithm { const capBot = addGroupToAlgorithmBar(true, capVG); addLabelToAlgorithmBar('Capacity:', capTop); - this.capacityField = addControlToAlgorithmBar('Text', String(DEFAULT_CAPACITY), capTop); - this.capacityField.size = 3; - this.capacityField.onkeydown = this.returnSubmit( - this.capacityField, - this.restartCallback.bind(this), - 2, + this.capacityField = this.addInputField( + capTop, + 3, + this.restartCallback, true, + String(DEFAULT_CAPACITY), ); - this.controls.push(this.capacityField); - - this.restartButton = addControlToAlgorithmBar('Button', 'Restart', capBot); - this.restartButton.onclick = this.restartCallback.bind(this); - this.controls.push(this.restartButton); + this.restartButton = this.addButton('Restart', this.restartCallback, capBot); addDivisorToAlgorithmBar(); // Clear button - this.clearButton = addControlToAlgorithmBar('Button', 'Clear'); - this.clearButton.onclick = this.clearCallback.bind(this); - this.controls.push(this.clearButton); + this.clearButton = this.addButton('Clear', this.clearCallback); } setup() { @@ -531,18 +499,21 @@ export default class LRU extends Algorithm { // Callbacks for the supported operations of LRU Cache + rejectInput(message, button) { + this.implementAction(this.setStatusCmd.bind(this), message); + this.shake(button); + } + putCallback() { this.clearInspection(); const key = this.putKeyField.value.trim(); const val = this.putValField.value.trim(); if (key === '' || val === '') { - this.implementAction(this.setStatusCmd.bind(this), STATUS.NEED_KEY_VALUE); - this.shake(this.putButton); + this.rejectInput(STATUS.NEED_KEY_VALUE, this.putButton); return; } if (FORBIDDEN_KEYS.has(key)) { - this.implementAction(this.setStatusCmd.bind(this), STATUS.FORBIDDEN_KEY(key)); - this.shake(this.putButton); + this.rejectInput(STATUS.FORBIDDEN_KEY(key), this.putButton); return; } this.putKeyField.value = ''; @@ -554,8 +525,7 @@ export default class LRU extends Algorithm { this.clearInspection(); const key = this.getKeyField.value.trim(); if (key === '') { - this.implementAction(this.setStatusCmd.bind(this), STATUS.NEED_GET_KEY); - this.shake(this.getButton); + this.rejectInput(STATUS.NEED_GET_KEY, this.getButton); return; } this.getKeyField.value = ''; @@ -566,8 +536,7 @@ export default class LRU extends Algorithm { this.clearInspection(); const key = this.deleteKeyField.value.trim(); if (key === '') { - this.implementAction(this.setStatusCmd.bind(this), STATUS.NEED_DEL_KEY); - this.shake(this.deleteButton); + this.rejectInput(STATUS.NEED_DEL_KEY, this.deleteButton); return; } this.deleteKeyField.value = ''; @@ -579,11 +548,7 @@ export default class LRU extends Algorithm { const raw = parseInt(this.capacityField.value, 10); // Enforce capacity between 1 and MAX_CAPACITY (explicitly disallow 0 or negative) if (isNaN(raw) || raw < 1 || raw > MAX_CAPACITY) { - this.implementAction( - this.setStatusCmd.bind(this), - STATUS.INVALID_CAPACITY(MAX_CAPACITY), - ); - this.shake(this.restartButton); + this.rejectInput(STATUS.INVALID_CAPACITY(MAX_CAPACITY), this.restartButton); return; } this.capacity = raw; From 27b4750c549f1d23ffbf0e5b5de10abf9389eb97 Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Sat, 26 Sep 2026 18:51:09 -0400 Subject: [PATCH 5/9] original unmodified Algorithm.js --- src/algo/Algorithm.js | 134 ++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 83 deletions(-) diff --git a/src/algo/Algorithm.js b/src/algo/Algorithm.js index 0511d8c..7ee5963 100644 --- a/src/algo/Algorithm.js +++ b/src/algo/Algorithm.js @@ -29,9 +29,7 @@ import { act } from '../anim/AnimationMain'; let controlTabNavigationInstalled = false; const getControlRoots = () => { - const algorithmRoot = - document.getElementById('algoControlSection') || - document.getElementById('AlgorithmSpecificControls'); + const algorithmRoot = document.getElementById('algoControlSection') || document.getElementById('AlgorithmSpecificControls'); const animationRoot = document.getElementById('GeneralAnimationControls'); const roots = [algorithmRoot, animationRoot].filter(Boolean); console.log('Control roots found:', roots.length, roots); @@ -41,68 +39,56 @@ const getControlRoots = () => { const getControlFocusables = () => { const roots = getControlRoots(); return roots - .flatMap(root => + .flatMap((root) => Array.from( root.querySelectorAll( - 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])', - ), - ), + 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) + ) ) - .filter(el => !el.disabled && el.offsetParent !== null); + .filter((el) => !el.disabled && el.offsetParent !== null); }; -const handleControlTabNavigation = event => { +const handleControlTabNavigation = (event) => { if (event.key !== 'Tab') return; - + const roots = getControlRoots(); const activeElement = document.activeElement; - + // Check if the active element is within a control root - if (!roots.some(root => root && root.contains(activeElement))) return; + if (!roots.some((root) => root && root.contains(activeElement))) return; const focusables = getControlFocusables(); - console.log( - 'Tab pressed. Active element:', - activeElement.tagName, - activeElement.type, - activeElement.value, - ); + console.log('Tab pressed. Active element:', activeElement.tagName, activeElement.type, activeElement.value); console.log('Focusables found:', focusables.length); focusables.forEach((f, i) => console.log(` [${i}]`, f.tagName, f.type, f.value)); console.log( - focusables.map(el => ({ - tag: el.tagName, - type: el.type, - visible: !!el.offsetParent, - tabIndex: el.tabIndex, - })), - ); - + focusables.map(el => ({ + tag: el.tagName, + type: el.type, + visible: !!el.offsetParent, + tabIndex: el.tabIndex + })) +); + if (focusables.length === 0) return; event.preventDefault(); const currentIndex = focusables.indexOf(activeElement); console.log('Current index in focusables:', currentIndex); - + // If active element not found, start from first const startIndex = currentIndex === -1 ? 0 : currentIndex; const direction = event.shiftKey ? -1 : 1; let nextIndex = startIndex + direction; - + if (nextIndex >= focusables.length) nextIndex = 0; if (nextIndex < 0) nextIndex = focusables.length - 1; - console.log( - 'Moving to index:', - nextIndex, - 'element:', - focusables[nextIndex].tagName, - focusables[nextIndex].type, - focusables[nextIndex].value, - ); + console.log('Moving to index:', nextIndex, 'element:', focusables[nextIndex].tagName, focusables[nextIndex].type, focusables[nextIndex].value); - focusables.forEach(el => el.classList.remove('tab-focused-input')); + focusables.forEach((el) => el.classList.remove('tab-focused-input')); const next = focusables[nextIndex]; next.focus(); next.classList.add('tab-focused-input'); @@ -193,12 +179,12 @@ export function addDropDownGroupToAlgorithmBar(optionNames, groupName, group, al group.appendChild(span); span.setAttribute('class', 'groupChild'); } - + // Attach Tab navigation if algorithm instance is provided if (algorithm) { algorithm.attachTabNavigation(dropDown); } - + return dropDown; } @@ -224,7 +210,7 @@ export function addRadioButtonGroupToAlgorithmBar(buttonNames, groupName, group, bottomLevel.appendChild(label); newTable.appendChild(midLevel); buttonList.push(button); - + // Attach Tab navigation if algorithm instance is provided if (algorithm) { algorithm.attachTabNavigation(button); @@ -264,19 +250,12 @@ export function addControlToAlgorithmBar(type, value, group, algorithm = null) { } // Add tabindex for proper keyboard focus - if ( - normalizedType === 'button' || - normalizedType === 'checkbox' || - normalizedType === 'radio' - ) { + if (normalizedType === 'button' || normalizedType === 'checkbox' || normalizedType === 'radio') { element.setAttribute('tabindex', '0'); } // Attach Tab navigation to buttons, checkboxes, radio buttons if algorithm instance is provided - if ( - algorithm && - (normalizedType === 'button' || normalizedType === 'checkbox' || normalizedType === 'radio') - ) { + if (algorithm && (normalizedType === 'button' || normalizedType === 'checkbox' || normalizedType === 'radio')) { algorithm.attachTabNavigation(element); } @@ -520,19 +499,16 @@ export default class Algorithm { } getNextFocusableInput(currentField, direction = 1) { - const algorithmRoot = - document.getElementById('algoControlSection') || - document.getElementById('AlgorithmSpecificControls'); + const algorithmRoot = document.getElementById('algoControlSection') || document.getElementById('AlgorithmSpecificControls'); const roots = [algorithmRoot, document.getElementById('GeneralAnimationControls')]; if (roots.length === 0) return null; - const allInputs = roots.flatMap(root => - Array.from( + const allInputs = roots + .flatMap((root) => Array.from( root.querySelectorAll( - 'input[type="text"], input[type="button"], input[type="checkbox"], input[type="radio"], select, [tabindex="0"]', - ), - ), - ); + 'input[type="text"], input[type="button"], input[type="checkbox"], input[type="radio"], select, [tabindex="0"]' + ) + )); if (allInputs.length === 0) return null; @@ -562,17 +538,13 @@ export default class Algorithm { } unhighlightInputFields() { - const algorithmRoot = - document.getElementById('algoControlSection') || - document.getElementById('AlgorithmSpecificControls'); - const roots = [algorithmRoot, document.getElementById('GeneralAnimationControls')].filter( - Boolean, - ); - roots.forEach(root => { + const algorithmRoot = document.getElementById('algoControlSection') || document.getElementById('AlgorithmSpecificControls'); + const roots = [algorithmRoot, document.getElementById('GeneralAnimationControls')].filter(Boolean); + roots.forEach((root) => { const allInputs = root.querySelectorAll( - 'input, button, select, textarea, [tabindex]:not([tabindex="-1"])', + 'input, button, select, textarea, [tabindex]:not([tabindex="-1"])' ); - allInputs.forEach(input => { + allInputs.forEach((input) => { input.classList.remove('tab-focused-input'); }); }); @@ -580,7 +552,7 @@ export default class Algorithm { attachTabNavigation(element, action = null) { // Attach Tab/Enter navigation to any focusable element - element.addEventListener('keydown', event => { + element.addEventListener('keydown', (event) => { const keyASCII = event.keyCode || event.which; // Handle Enter key - trigger click for buttons, or action function @@ -606,7 +578,7 @@ export default class Algorithm { if (maxsize !== undefined) { field.size = maxsize; } - return event => { + return (event) => { let keyASCII = 0; if (window.event) { // IE @@ -634,21 +606,17 @@ export default class Algorithm { !sizeGood || (intOnly && !( - ( - (keyASCII >= 48 && keyASCII <= 57) || // 0-9 - (keyASCII === 189 && field.value.length === 0) || // minus at start - (keyASCII >= 96 && keyASCII <= 105) - ) // numpad 0-9 + (keyASCII >= 48 && keyASCII <= 57) || // 0-9 + (keyASCII === 189 && field.value.length === 0) || // minus at start + (keyASCII >= 96 && keyASCII <= 105) // numpad 0-9 )) || (!intOnly && !( - ( - (keyASCII >= 48 && keyASCII <= 57) || // 0-9 - (keyASCII >= 65 && keyASCII <= 90) || // A-Z - (keyASCII >= 97 && keyASCII <= 122) || // a-z - (keyASCII === 189 && field.value.length === 0) || // minus at start - (keyASCII >= 96 && keyASCII <= 105) - ) // numpad 0-9 + (keyASCII >= 48 && keyASCII <= 57) || // 0-9 + (keyASCII >= 65 && keyASCII <= 90) || // A-Z + (keyASCII >= 97 && keyASCII <= 122) || // a-z + (keyASCII === 189 && field.value.length === 0) || // minus at start + (keyASCII >= 96 && keyASCII <= 105) // numpad 0-9 )) ) { if (!controlKey(keyASCII)) return false; @@ -698,7 +666,7 @@ Algorithm.prototype.returnSubmitFloat = function (field, funct, maxsize) { if (maxsize !== undefined) { field.size = maxsize; } - return event => { + return (event) => { let keyASCII = 0; if (window.event) { // IE @@ -749,4 +717,4 @@ Algorithm.prototype.returnSubmitFloat = function (field, funct, maxsize) { Algorithm.prototype.addReturnSubmit = function (field, action) { field.onkeydown = this.returnSubmit(field, action, 4, false); -}; +}; \ No newline at end of file From 1038b3a206e36bfe0f58284d65bf0261a4b877ff Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Sat, 26 Sep 2026 18:52:03 -0400 Subject: [PATCH 6/9] space --- src/algo/Algorithm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algo/Algorithm.js b/src/algo/Algorithm.js index 7ee5963..70507ef 100644 --- a/src/algo/Algorithm.js +++ b/src/algo/Algorithm.js @@ -717,4 +717,4 @@ Algorithm.prototype.returnSubmitFloat = function (field, funct, maxsize) { Algorithm.prototype.addReturnSubmit = function (field, action) { field.onkeydown = this.returnSubmit(field, action, 4, false); -}; \ No newline at end of file +}; From 6096c8539ac87d5486effe3f02df3ca2cc986c9c Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Sat, 26 Sep 2026 19:33:40 -0400 Subject: [PATCH 7/9] name change --- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3ac4cef --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + pull_request: + branches: + - master + types: [opened, synchronize, reopened] + +permissions: + contents: write + +jobs: + run-ci: + runs-on: ubuntu-latest + steps: + # Checkout Action: https://github.com/actions/checkout + - name: Checkout repository + uses: actions/checkout@v7 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + # Setup Node Action: https://github.com/actions/setup-node + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 16.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run Prettier + run: npm run prettier + + # Auto Commit Action Repo: https://github.com/stefanzweifel/git-auto-commit-action + - name: Commit and push formatting changes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "style: auto-format code with prettier" + commit_user_name: "github-actions[bot]" + commit_user_email: "github-actions[bot]@users.noreply.github.com" + + - name: Run ESLint + run: npm run lint From cfda9f73a03ac101302c36c5e8d56331b666919d Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Fri, 2 Oct 2026 21:07:54 -0400 Subject: [PATCH 8/9] spacing --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 457c08d..c8e0cdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,4 +38,4 @@ jobs: # ESLint: Enforces style and syntax rules - name: Run ESLint - run: npm run lint \ No newline at end of file + run: npm run lint From 87c31ae68ad711c4303d07af7b9613d156330133 Mon Sep 17 00:00:00 2001 From: karkir0003 Date: Tue, 13 Oct 2026 23:31:03 -0400 Subject: [PATCH 9/9] Add LRU to expected exports in algo index test --- src/algo/__tests__/index.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/algo/__tests__/index.test.js b/src/algo/__tests__/index.test.js index a5d0c32..16f199d 100644 --- a/src/algo/__tests__/index.test.js +++ b/src/algo/__tests__/index.test.js @@ -30,6 +30,7 @@ describe('algo index exports', () => { 'KMP', 'Kruskals', 'LCS', + 'LRU', 'LSDRadix', 'LVA', 'LinkedList',