Last active
March 21, 2025 01:48
-
-
Save vyznev/bb2c1adb6e96eb65a87bab3822c74e81 to your computer and use it in GitHub Desktop.
Adds an estimate of "hotness" to the question sidebar, calculated using the formula from http://meta.stackexchange.com/a/61343. Questions with a high hotness value may be selected for the Hot Network Questions list. Note that the hotness value displayed by this script does not include the per-site scaling factors, and so does not match the "arbi…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Stack Exchange hotness estimator | |
// @namespace http://vyznev.net/ | |
// @description Estimates how highly each Stack Exchange question would rank on the Hot Network Questions list | |
// @author Ilmari Karonen | |
// @version 0.4.5 | |
// @license Public domain | |
// @homepageURL https://meta.stackexchange.com/a/284933 | |
// @downloadURL https://gist.github.com/vyznev/bb2c1adb6e96eb65a87bab3822c74e81/raw/se_hotness_estimator.user.js | |
// @match *://*.stackexchange.com/questions/* | |
// @match *://*.stackoverflow.com/questions/* | |
// @match *://*.superuser.com/questions/* | |
// @match *://*.serverfault.com/questions/* | |
// @match *://*.stackapps.com/questions/* | |
// @match *://*.mathoverflow.net/questions/* | |
// @match *://*.askubuntu.com/questions/* | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
var voteCount = document.querySelector('#question .js-vote-count'); | |
var infobar = document.querySelector('#question-header + div'); // no id :( | |
if ( ! voteCount || ! infobar || /(mainbar|sidebar)/.test(infobar.id) ) return; | |
var creationTimeStamp = infobar.querySelector('time[itemprop=dateCreated]'); | |
if ( ! creationTimeStamp ) return; | |
var now = new Date (); | |
var questionScore = Number(voteCount.textContent); | |
var qCreationTimeStamp = creationTimeStamp.getAttribute('datetime'); | |
if (!/([A-Z]|[-+][0-9:]+)$/.test(qCreationTimeStamp)) qCreationTimeStamp += 'Z'; // Assume UTC if there doesn't seem to be any timezone indicator. | |
var qCreationTime = new Date(qCreationTimeStamp); | |
var qAgeInHours = (now.getTime() - qCreationTime.getTime()) / (1000*60*60); | |
var answerScores = document.querySelectorAll('.answer:not(.deleted-answer) .js-vote-count'); | |
var answerCount = answerScores.length; | |
var answerScore = 0; | |
for (var i = 0; i < answerScores.length; i++) answerScore += Number(answerScores[i].textContent); | |
// http://meta.stackexchange.com/questions/60756/how-do-the-arbitrary-hotness-points-work-on-the-new-stack-exchange-home-page | |
// The conversion to percentages below is completely arbitrary, but seems to yield reasonable-looking values (i.e. around 100% for actual HNQs). | |
// Note: For questions that are ineligible for HNQ due to being less than 8 hours old, the hotness is calculated as if their age was 8 hours. | |
var hotness = ((Math.min(answerCount, 10) * questionScore) / 5 + answerScore) / Math.pow(Math.max(qAgeInHours, 8) + 1, 1.4); | |
var ineligibleBecause = []; | |
if (answerCount < 1) ineligibleBecause.push('no answers'); | |
if (qAgeInHours < 8) ineligibleBecause.push('age < 8h'); | |
if (qAgeInHours > 30*24) ineligibleBecause.push('age > 30d'); | |
var mathJaxInTitle = document.querySelector('#question-header h1[itemprop=name] script[type*=math]'); | |
if (mathJaxInTitle) ineligibleBecause.push('MathJax in title'); | |
if (/(^|\.)meta\./.test(location.hostname)) ineligibleBecause.push('on meta site'); | |
if (location.hostname === 'stackapps.com') ineligibleBecause.push('on StackApps'); | |
if ((window.StackExchange?.options?.locale || 'en') !== 'en') ineligibleBecause.push('not English'); | |
var noticeHeaders = document.querySelectorAll('#question aside.s-notice[role=status] b'); | |
if (Array.prototype.some.call(noticeHeaders, e => /\b(closed|on hold|duplicate|already has answers)\b/i.test(e.textContent))) ineligibleBecause.push('closed') | |
if (document.querySelector('#question.deleted-answer')) ineligibleBecause.push('deleted'); | |
// Checks for other ineligibility reasons could be added here, but would require scraping additional data off the page or getting it from the SE API. | |
var title = 'The relative hotness score of this question is approximately ' + hotness.toPrecision(5) + '.'; | |
if (ineligibleBecause.length > 0) title += ' (Not eligible for HNQ because: ' + ineligibleBecause.join(', ') + '.)'; | |
var oldRow = infobar.firstElementChild, newRow = oldRow.cloneNode(false), keySpan = oldRow.firstElementChild.cloneNode(false); | |
keySpan.textContent = 'Hotness'; | |
newRow.innerHTML = ' <span>' + Math.round(100 * hotness) + '%</span>'; | |
if (ineligibleBecause.length > 0) newRow.firstElementChild.style.textDecoration = 'line-through'; | |
newRow.insertBefore(keySpan, newRow.firstChild); | |
newRow.setAttribute('title', title); | |
infobar.lastElementChild.className += ' mr16'; | |
infobar.appendChild(newRow); | |
} )(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment