// Ebay Cost/TB Calculator & Sorter V2.6 // For use with Tampermonkey @require directive. // Original author: Your Name / AI Assistant // Description: Calculates and displays cost per TB for eBay listings, allows sorting, // handles ambiguous titles, and includes unit tests for parsing logic. (function() { 'use strict'; const DEBUG_MODE = true; // Set to true to run unit tests on load // --- Global Variables --- let originalOrderMap = new Map(); // Stores { itemId: originalIndex } let isSorted = false; let mainListParentElement = null; // Cached parent element let observer = null; // MutationObserver instance let observerTargetNode = null; // Node the observer is attached to const observerConfig = { childList: true, subtree: true }; // Observer configuration let processTimer = null; // Timer for debouncing observer callback // --- Core Parsing Functions --- function parseSizeAndQuantity(title) { title = title.toUpperCase(); let totalTB = 0; let quantity = 1; // Default to 1 let needed_description_check = false; let individualSizeTB = 0; // 1. Parse Quantity - More conservative approach // Prioritize explicit "LOT OF N", "PACK OF N", "N PACK", "N LOT", "BULK N" const explicitQtyPatterns = [ /\b(?:LOT\s+OF|LOT)\s*\(?\s*(\d+)\s*\)?/i, // e.g., "LOT OF (9)", "LOT (9)", "LOT 9" /\b(?:LOT\s+OF|LOT)\s*\*\s*(\d+)/i, // e.g., "Lot of*10" /\b(?:PACK\s+OF|PACK|BULK)\s*\(?\s*(\d+)\s*\)?/i, // e.g., "PACK OF 5", "5 PACK", "BULK 5" /\b(\d+)\s*-\s*PACK\b/i, // e.g., "10-PACK" /\b(\d+)\s*COUNT\b/i // e.g., "10 COUNT" ]; for (const pattern of explicitQtyPatterns) { const qtyMatch = title.match(pattern); if (qtyMatch && qtyMatch[1]) { const parsedQty = parseInt(qtyMatch[1], 10); // Added a sanity check for quantity (e.g., not excessively large from a model number) if (parsedQty > 0 && parsedQty < 500) { quantity = parsedQty; break; // Found a valid quantity } } } // 2. Parse Size const sizeMatches = []; // Refined regex: TB/GB must be followed by a word boundary or specific terminators (space, hyphen, comma, parens, end of string) // This prevents matching "GB" in "6GB/S" or "6GBPS" const sizeRegex = /(\d+(?:\.\d+)?)\s*(TB|GB)(?:\b|(?=\s|-|,|\(|\)|$))/g; let match; while ((match = sizeRegex.exec(title)) !== null) { sizeMatches.push({ value: parseFloat(match[1]), unit: match[2].toUpperCase() }); } if (sizeMatches.length > 0) { const uniqueSizesTB = [...new Set( sizeMatches.map(sm => sm.unit === 'GB' ? sm.value / 1000 : sm.value) )].sort((a, b) => a - b); if (uniqueSizesTB.length > 0) { individualSizeTB = uniqueSizesTB[0]; // Pick the smallest detected size if (uniqueSizesTB.length > 1) { needed_description_check = true; // Multiple different sizes mentioned } } } // 3. Further checks for ambiguity for needed_description_check if (title.match(/\d+(?:\.\d+)?\s*(?:GB|TB)\s*(?:-|&|OR|TO)\s*\d+(?:\.\d+)?\s*(?:GB|TB)/i)) { needed_description_check = true; // e.g. "120GB-250GB" or "120GB & 250GB" } if (quantity > 1 && title.includes("MIXED")) { needed_description_check = true; } // If "CHECK DESCRIPTION" or similar is present, and there's ambiguity in quantity or size if (title.includes("CHECK THE DESCRIPTION") || title.includes("CHECK DESCRIPTION") || title.includes("SEE DESCRIPTION")) { if (quantity > 1 || sizeMatches.length === 0 || sizeMatches.length > 1) { needed_description_check = true; } } if (individualSizeTB > 0) { totalTB = individualSizeTB * quantity; } // If it's a lot (quantity > 1) and no size could be determined (totalTB is 0), flag for check. if (quantity > 1 && totalTB === 0) { needed_description_check = true; } // If only one size was found and quantity is 1, ensure check is false unless other flags hit if (quantity === 1 && sizeMatches.length === 1 && !needed_description_check) { needed_description_check = false; } return { totalTB: parseFloat(totalTB.toFixed(4)), quantity, needed_description_check }; // Return totalTB with more precision } function parsePrice(priceText) { if (priceText.toLowerCase().includes(' to ')) { return null; // Price range, cannot calculate } const priceMatch = priceText.match(/\$?([\d,]+\.?\d*)/); if (priceMatch) { return parseFloat(priceMatch[1].replace(/,/g, '')); } return null; } // --- Unit Testing --- function runUnitTests() { console.log("Ebay Cost/TB: --- Running Unit Tests for parseSizeAndQuantity ---"); const testCases = [ { title: "LOT OF (9) MAJOR BRAND 2.5\" 7MM SSD * Kingston, Samsung, SanDisk& PNY*120-250GB", expected: { totalTB: 9 * 0.120, quantity: 9, needed_description_check: true } }, { title: "Lot of 10 Intel 256 GB 2.5\" SATA SSD different Model check the Description", expected: { totalTB: 10 * 0.256, quantity: 10, needed_description_check: true } }, { title: "Lot of*10 Mixed brands 240GB-256GB 2.5\" SATA SSD Drives Working & tested", expected: { totalTB: 10 * 0.240, quantity: 10, needed_description_check: true } }, { title: "Lot of 9 SSD 120&128 GB 2.5\" SATA different brands check the description", expected: { totalTB: 9 * 0.120, quantity: 9, needed_description_check: true } }, { title: "Bulk 5 Lot Samsung 870 EVO 500GB SSD SATA - Used - Tested Passed Smart Test", expected: { totalTB: 5 * 0.500, quantity: 5, needed_description_check: false } }, { title: "Samsung 1.6TB NVME PCIe 3.0 x8 2.75\" SSD MZPLK1T6HCHP PM1725 Series TLC", expected: { totalTB: 1.6, quantity: 1, needed_description_check: false } }, { title: "Brand New Crucial X6 2TB Portable External SSD (CT2000X6SSD9)", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } }, { title: "Western Digital WD_BLACK SN850X 2TB NVMe Internal SSD", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } }, { title: "Corsair Force Series MP600 1TB Gen4 PCIe X4 NVMe M.2 SSD Up to 4950 MB/s CSSD...", expected: { totalTB: 1.0, quantity: 1, needed_description_check: false } }, { title: "Micron 5100 MAX 1.84TB SATA 6Gb/s 2.5\" SSD MTFDDAK1T9TCC-1AR1ZABYY", expected: { totalTB: 1.84, quantity: 1, needed_description_check: false } }, { title: "Dell 0HGX92 1.6TB 2.5” PCIe NVMe Gen4 SSD Intel D7-P5600 SSDPF2KE016T9T HGX92 ES", expected: { totalTB: 1.6, quantity: 1, needed_description_check: false } }, { title: "2 X 4TB SSDs Lot", expected: { totalTB: 4.0, quantity: 1, needed_description_check: false } }, // Current simplified Q parsing will result in Q=1, totalTB=4TB. To get Q=2 needs more complex logic. { title: "LOT OF 2X 1TB SSDs", expected: { totalTB: 2 * 1.0, quantity: 2, needed_description_check: false } }, // Should be caught by "LOT OF (N)" if "2X" is seen as "2" { title: "10-PACK 1TB SSD", expected: { totalTB: 10 * 1.0, quantity: 10, needed_description_check: false } }, { title: "SSD 5 COUNT 256GB", expected: { totalTB: 5 * 0.256, quantity: 5, needed_description_check: false } }, { title: "Single 2TB Drive", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } }, { title: "Lot of 2 (512GB SSDs)", expected: { totalTB: 2 * 0.512, quantity: 2, needed_description_check: false } }, { title: "Mixed Lot SSDs 120GB, 240GB, 500GB - Total 3 drives", expected: { totalTB: 0.120, quantity: 1, needed_description_check: true } } // Q parsing for "Total N drives" not implemented, smallest size taken. Q will be 1. ]; let testsPassed = 0; let testsFailed = 0; testCases.forEach((test, index) => { const result = parseSizeAndQuantity(test.title); // Using a small tolerance for floating point comparison of totalTB const totalTBCheck = Math.abs(result.totalTB - test.expected.totalTB) < 0.0001; const quantityCheck = result.quantity === test.expected.quantity; const neededCheck = result.needed_description_check === test.expected.needed_description_check; if (totalTBCheck && quantityCheck && neededCheck) { // console.log(`Test ${index + 1}: PASSED - "${test.title}"`); testsPassed++; } else { console.error(`Test ${index + 1}: FAILED - "${test.title}"`); console.error(` Expected: totalTB=${test.expected.totalTB.toFixed(4)}, Q=${test.expected.quantity}, Check=${test.expected.needed_description_check}`); console.error(` Actual: totalTB=${result.totalTB.toFixed(4)}, Q=${result.quantity}, Check=${result.needed_description_check}`); testsFailed++; } }); console.log(`--- Unit Test Summary: ${testsPassed} Passed, ${testsFailed} Failed ---`); } // --- DOM Manipulation & Display --- function addStyles() { const css = ` li.s-item, li.srp-results__item { position: relative !important; overflow: visible !important; padding-bottom: 5px !important; } .cost-per-tb-info { position: absolute; right: 10px; top: 40px; background-color: rgba(0, 100, 0, 0.85); color: white; padding: 4px 8px; border-radius: 5px; z-index: 1000; font-size: 0.9em; font-weight: bold; text-align: center; box-shadow: 1px 1px 3px rgba(0,0,0,0.5); transition: transform 0.2s ease; } .cost-per-tb-info:hover { transform: scale(1.05); } .cost-per-tb-info small.check-desc-note { color: #FFD700; /* Gold */ font-weight: bold; } #costPerTbSortControl { display: inline-block; margin-left: 20px; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; background-color: #f0f0f0; vertical-align: middle; color: #333; cursor: pointer; font-size: 14px; } #costPerTbSortControl:hover { background-color: #e9e9e9; } #costPerTbSortControl input[type="checkbox"] { margin-right: 8px; vertical-align: middle; } #costPerTbSortControl label { cursor: pointer; vertical-align: middle; color: #333; } `; try { const styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = css; document.head.appendChild(styleSheet); } catch (e) { console.error("Ebay Cost/TB: Failed to add styles:", e); } } function getItemSelector() { if (document.querySelector('li.s-item')) return 'li.s-item'; if (document.querySelector('li.srp-results__item')) return 'li.srp-results__item'; if (document.querySelector('div.s-item')) return 'div.s-item'; return 'li[class*="s-item"], div[class*="s-item"]'; } function getMainListParent() { if (mainListParentElement && document.body.contains(mainListParentElement)) { return mainListParentElement; } mainListParentElement = document.querySelector('ul.srp-results, div#srp-river-results ul.srp-results'); if (!mainListParentElement) { mainListParentElement = document.querySelector('div#srp-river-results ul'); } if (!mainListParentElement) { const itemSelector = getItemSelector(); const containers = document.querySelectorAll('ul, div'); for (const container of containers) { if (container.querySelector(itemSelector)) { const directChildren = Array.from(container.children).filter(child => child.matches(itemSelector)); if (directChildren.length > 1) { mainListParentElement = container; break; } } } } if (!mainListParentElement) { console.error("Ebay Cost/TB: CRITICAL - Could not find main list parent element."); } return mainListParentElement; } function storeOriginalOrder(itemsNodeList) { if (originalOrderMap.size > 0 && itemsNodeList.length === originalOrderMap.size) { return; } originalOrderMap.clear(); itemsNodeList.forEach((itemElement, index) => { let itemId = itemElement.id; if (!itemId) { const linkWithItemId = itemElement.querySelector('a[href*="/itm/"]'); if (linkWithItemId && linkWithItemId.href) { const match = linkWithItemId.href.match(/\/itm\/(\d+)/); if (match && match[1]) { itemId = "ebayitem_" + match[1]; itemElement.id = itemId; } } } if (itemId && !originalOrderMap.has(itemId)) { originalOrderMap.set(itemId, index); } else if (!itemId) { // console.warn("Ebay Cost/TB: Item found without a usable ID during storeOriginalOrder.", itemElement); } }); // console.log("Ebay Cost/TB: Stored original order for", originalOrderMap.size, "items."); } function processResults() { const parent = getMainListParent(); if (!parent) return; const itemSelector = getItemSelector(); const itemsOnPage = parent.querySelectorAll(itemSelector + ':not(.cost-per-tb-processed-flag)'); itemsOnPage.forEach(item => { const titleElement = item.querySelector('.s-item__title, .srp-results__title'); const priceElement = item.querySelector('.s-item__price, .srp-results__price'); if (titleElement && priceElement) { const title = titleElement.innerText; const priceText = priceElement.innerText; const price = parsePrice(priceText); const parsedInfo = parseSizeAndQuantity(title); // Returns totalTB with more precision const totalTB = parsedInfo.totalTB; const needed_description_check = parsedInfo.needed_description_check; item.dataset.neededDescriptionCheck = String(needed_description_check); let displayElement = item.querySelector('.cost-per-tb-info'); if (!displayElement) { displayElement = document.createElement('div'); displayElement.className = 'cost-per-tb-info'; const imageContainer = item.querySelector('.s-item__image-section, .s-item__image'); if (imageContainer && imageContainer.parentNode) { imageContainer.parentNode.style.position = 'relative'; imageContainer.parentNode.appendChild(displayElement); } else { item.appendChild(displayElement); } } if (price !== null && totalTB > 0) { const costPerTB = price / totalTB; if (needed_description_check) { item.dataset.costPerTb = '9999999'; // Sentinel for sorting // Display totalTB.toFixed(2) for consistency in UI displayElement.innerHTML = `$${costPerTB.toFixed(2)}* / TB
(${totalTB.toFixed(2)} TB, Check Desc.)`; } else { item.dataset.costPerTb = costPerTB; displayElement.innerHTML = `$${costPerTB.toFixed(2)} / TB
(${totalTB.toFixed(2)} TB)`; } } else { item.dataset.costPerTb = '9999999'; let ambiguousNote = needed_description_check || (parsedInfo.quantity > 1 && totalTB === 0) ? "(Check Desc.)" : "(Details N/A)"; // Display totalTB.toFixed(2) if totalTB > 0 but price is null, else ambiguousNote let tbDisplay = totalTB > 0 ? `(${totalTB.toFixed(2)} TB, Price N/A)` : ambiguousNote; if (totalTB === 0 && parsedInfo.quantity > 1) tbDisplay = ambiguousNote; // Explicitly show check desc if lot with no size else if (totalTB === 0) tbDisplay = "(Size N/A)"; displayElement.innerHTML = `Details unclear
${tbDisplay}`; } if(displayElement.querySelector('span')) displayElement.querySelector('span').style.fontSize = '0.8em'; if(displayElement.querySelector('small')) displayElement.querySelector('small').style.fontSize = '0.7em'; } else { item.dataset.costPerTb = '9999999'; } item.classList.add('cost-per-tb-processed-flag'); item.dataset.costPerTbProcessed = 'true'; }); const allItems = parent.querySelectorAll(itemSelector); if (originalOrderMap.size === 0 || Math.abs(originalOrderMap.size - allItems.length) > 5) { if (allItems.length > 0) storeOriginalOrder(allItems); } if (isSorted) { sortResults(); } } function sortResults() { const parent = getMainListParent(); if (!parent) return; const itemSelector = getItemSelector(); let itemsArray = Array.from(parent.querySelectorAll(itemSelector)); if (originalOrderMap.size === 0 && itemsArray.length > 0) { storeOriginalOrder(itemsArray); } itemsArray.sort((a, b) => { const costA = parseFloat(a.dataset.costPerTb || '9999999'); const costB = parseFloat(b.dataset.costPerTb || '9999999'); return costA - costB; }); if (observer) observer.disconnect(); itemsArray.forEach(item => parent.appendChild(item)); if (observer && observerTargetNode) { observer.observe(observerTargetNode, observerConfig); } else if (observer && !observerTargetNode) { setupObserver(); } // console.log("Ebay Cost/TB: Sorted by cost per TB."); } function restoreOriginalOrder() { const parent = getMainListParent(); if (!parent) return; if (originalOrderMap.size === 0) { const itemSelector = getItemSelector(); const currentItems = parent.querySelectorAll(itemSelector); if (currentItems.length > 0) storeOriginalOrder(currentItems); if (originalOrderMap.size === 0) return; } let itemsToReorder = []; originalOrderMap.forEach((originalIndex, itemId) => { const itemElement = document.getElementById(itemId) || parent.querySelector(`[id="${itemId}"]`); if (itemElement && parent.contains(itemElement)) { itemsToReorder.push({ element: itemElement, originalIndex: originalIndex, id: itemId }); } }); const itemSelector = getItemSelector(); const currentItemsInDOM = parent.querySelectorAll(itemSelector); let newItemsCount = 0; currentItemsInDOM.forEach(domItem => { let itemId = domItem.id; if (!itemId) { const linkWithItemId = domItem.querySelector('a[href*="/itm/"]'); if (linkWithItemId && linkWithItemId.href) { const match = linkWithItemId.href.match(/\/itm\/(\d+)/); if (match && match[1]) itemId = "ebayitem_" + match[1]; } } if (itemId && !originalOrderMap.has(itemId)) { itemsToReorder.push({ element: domItem, originalIndex: 900000 + (Array.from(domItem.parentNode.children).indexOf(domItem)), id: itemId || 'new_item_' + newItemsCount++ }); } else if (!itemId && !itemsToReorder.some(entry => entry.element === domItem)) { itemsToReorder.push({ element: domItem, originalIndex: 950000 + (Array.from(domItem.parentNode.children).indexOf(domItem)), id: 'no_id_item_' + newItemsCount++ }); } }); itemsToReorder.sort((a, b) => a.originalIndex - b.originalIndex); if (observer) observer.disconnect(); itemsToReorder.forEach(entry => parent.appendChild(entry.element)); if (observer && observerTargetNode) { observer.observe(observerTargetNode, observerConfig); } else if (observer && !observerTargetNode) { setupObserver(); } // console.log("Ebay Cost/TB: Restored original order."); } function handleSortToggle(event) { const checkbox = event.currentTarget; if (checkbox.checked) { isSorted = true; sortResults(); } else { isSorted = false; restoreOriginalOrder(); } } function addSortControl() { let controlsContainer = document.querySelector('.srp-controls__sort- σήμερα') || document.querySelector('.srp-controls__sort') || document.querySelector('div[class*="srp-controls__sort"]'); if (!controlsContainer) { controlsContainer = document.querySelector('.srp-sort, .srp-controls'); if (controlsContainer && controlsContainer.firstChild && controlsContainer.firstChild.nodeName === "UL") { controlsContainer = controlsContainer.firstChild; } } if (!controlsContainer) { const fallbackContainer = document.querySelector('.srp-river-main') || document.querySelector('#srp-river-results') || document.body; if (fallbackContainer) { controlsContainer = document.createElement('div'); controlsContainer.style.textAlign = 'center'; controlsContainer.style.margin = '10px 0'; fallbackContainer.insertBefore(controlsContainer, fallbackContainer.firstChild); } else { return; } } if (document.getElementById('costPerTbSortControlWrapper')) return; const controlWrapper = document.createElement('div'); controlWrapper.id = 'costPerTbSortControlWrapper'; controlWrapper.style.display = 'inline-block'; controlWrapper.style.marginLeft = '20px'; controlWrapper.style.verticalAlign = 'middle'; const controlDiv = document.createElement('div'); controlDiv.id = 'costPerTbSortControl'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'costPerTbSortCheckbox'; checkbox.checked = isSorted; checkbox.style.verticalAlign = 'middle'; const label = document.createElement('label'); label.htmlFor = 'costPerTbSortCheckbox'; label.innerText = 'Sort by $/TB'; label.style.verticalAlign = 'middle'; label.style.marginLeft = '5px'; controlDiv.appendChild(checkbox); controlDiv.appendChild(label); controlWrapper.appendChild(controlDiv); controlDiv.addEventListener('click', (e) => { if (e.target !== checkbox) { checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change', { bubbles: true })); } }); checkbox.addEventListener('change', handleSortToggle); if (controlsContainer.nextSibling) { controlsContainer.parentNode.insertBefore(controlWrapper, controlsContainer.nextSibling); } else { controlsContainer.parentNode.appendChild(controlWrapper); } } function setupObserver() { observerTargetNode = getMainListParent(); if (!observerTargetNode) { observerTargetNode = document.querySelector('#srp-river-results') || document.body; } if (!observerTargetNode) return; const itemSelector = getItemSelector(); const callback = function(mutationsList, obs) { let needsProcessing = false; for(const mutation of mutationsList) { if (mutation.type === 'childList') { if (mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === 1 && !node.classList.contains('cost-per-tb-info') && (node.matches && node.matches(itemSelector) || (node.querySelector && node.querySelector(itemSelector)))) { needsProcessing = true; } }); } if (mutation.removedNodes.length > 0 && !isSorted) { mutation.removedNodes.forEach(node => { if (node.nodeType === 1 && node.matches && node.matches(itemSelector)) { needsProcessing = true; originalOrderMap.clear(); } }); } } if (!document.getElementById('costPerTbSortControlWrapper')) { const sortControlsArea = document.querySelector('.srp-controls__sort, div[class*="srp-controls__sort"], .srp-sort, .srp-controls'); if (sortControlsArea) needsProcessing = true; } } if (needsProcessing) { clearTimeout(processTimer); processTimer = setTimeout(() => { processResults(); addSortControl(); }, 750); } }; if (observer) observer.disconnect(); observer = new MutationObserver(callback); observer.observe(observerTargetNode, observerConfig); // console.log("Ebay Cost/TB: MutationObserver set up on:", observerTargetNode); } function init() { console.log("Ebay Cost/TB V2.6 (for Tampermonkey @require) starting..."); if (DEBUG_MODE) { runUnitTests(); } addStyles(); setTimeout(() => { processResults(); addSortControl(); setupObserver(); }, 2000); } if (document.readyState === "complete" || document.readyState === "interactive") { init(); } else { window.addEventListener('load', init); } })();