Hacker News sort by best

8 min read Original article ↗
'use strict'; // This let statement selects elements from the HTML document using a CSS selector. // Specifically, we're making an Array of all table rows (each news listing is // a table row in an html table). The css selector here says, find the 'tr' elements // that are nested at any depth below a tbody which is nested at any depth below any // element bearing the class 'itemlist' -- the dot in front of itemlist indicates // we're talking about a CSS class, not an element. // Look up css selectors and the document.querySelectorAll // function for more about that. // When you do a querySelectorAll, you get a NodeList in return, not quite the same // thing as an array, because you can't slice it. Well, maybe you could using css selectors // but lets not get into that. The 'Array.from' here converts our nodelist into and array // we can slice on the very next line. let elements = Array.from(document.querySelectorAll(".itemlist tbody tr")); // Since we'll be taking each news entry as a group of 3 tr's, we want to take the slice of // the tr's that is divisible by 3, the remaining 0-2 elements we'll call 'leftovers' // and append them back to the table after after everything else is done. let leftoverCount = elements.length - elements.length % 3; // Here, we're making a new iterator of the 'elements' array from the line above. // entries() is a built-in function that iterates over each element in the array and // pairs it with it's numeric index. So ['a', 'b'] becomes [[0, 'a'], [1, 'b']]. let ielements = elements.slice(0, leftoverCount).entries(); // Here we save the leftovers to be appended at the end. let leftovers = elements.slice(leftoverCount); // Just initializing some variables that will be used in the upcoming lines. // 'i' will be the index variable and 'e' will be the HTMLElement as we iterate through. // ielements from the line above. 'score' is the score of the line let i, e, score, rows = [], tbody = document.createElement("tbody"); // This is some hot new es6 magic right here. Remember how ielements turned our elements // array into an array of 2-element arrays, each containing the index and element // from the original array? Well, now we're unpacking that shit. You could implement // the same behavior using a simple counter variable starting at 0 and ticking up by // one with each iteration. I just find this approach more elegant and closer to the // way I would do it in Python. At the start of each iteration in this for loop, 'i' will // be the index and 'e' will be the table row element we want to examine. See // Destructuring assignment https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment for ([i, e] of ielements) { // Each news entry is represented as 3 consecutive 'tr' elements and the middle tr contains the sweet, juicy // score value we want to sort by. So, as we iterate over all the elements, we'll skip the 0th, take // the 1st, skip the 2nd, skip the 3rd, take the 4th. But what function represents the selection we // want to make? Mod division n % d gives you the remainder of n / d. Watch what happens: // 0 % 3 = 0 // 1 % 3 = 1 // 2 % 3 = 2 // 3 % 3 = 0 // 4 % 3 = 1 // 5 % 3 = 2 // 6 % 3 = 0 // Doing a mod division by 3 for all the natural numbers creates a repeating cycle of 0, 1, 2 for all the // numbers going on forever. In fact, replace 3 for n and the cycle will always be 0, 1, 2... up to n - 1. // Since we always want the middle element out of a set of 3, we want to take the element each time // the counter mod 3 equals 1. if (i % 3 === 1) { // So we're inside this if statement, so 'e' here must be one of those middle elements we were // looking for. We find the element with the css class of 'score' within our element, 'e' and // assign it to the variable scoreTag. Remember that querySelectorAll returns an Array, so // we index into it at its zeroth position to take what we assume is to be the only element in // the array. Which we can safely assume it will be, unless HN changes their layout. let scoreTag = e.querySelectorAll(".score")[0]; // Now that we have the score element, we need to extract the raw integer value of the score so that // we can sort the rows based on it. There are 2 big concepts introduced on this line. Ternary // Expressions and Regular Expressions (regexes). This line is dense. // A ternary expression is shorthand for doing a simple variable assignment based on an // if statement. It follows the format: // result = (condition)?expr1:expr2 // where condition is evaluated as a boolean true/false value and if it is true, then result will get // the value of expr1, otherwise result will be expr2. // We know that scoreTag will either be an HTMLElement or undefined. HTMLElement will evaluate to true, // undefined to false. If true, we're saying here we want to parse an integer out of whatever our regex // matched when executed on our scoreTag's innerText (the visible score text, ie. "27 points"). // Otherwise, we assume a score of 0. This places any abnormal content we may encounter at the bottom // of the list. But the 'abnormal content' is hypothetical and should never actully exist. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator // Okay now regexes are cool as shit. It's a whole language (or more broadly outside of js, a collection // of languages) that describe how to parse text and extract information. Like, grab all the digits // or find all words that start with a certain prefix within some text. Can't type it all here, // if you read any of these links, read the shit out of this one. It's insanely powerful. You will // be capable of things that mere mortals will worship you for. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions // Putting it together now, the parseInt function converts a string to an integer given some base. // In this case, being that we're normal humans, we want to use base 10. Yes it is mandatory to specify that. // So, we're defining the regex, /\d+/ which says grab the first sequential set of digits. If we executed that // on scoreTag.innerText, and the text was "27 points", we'd have the string "27". So, parseInt would // process that string and return the number 27 and that's what would be our score. Phew. let score = scoreTag ? parseInt(/\d+/.exec(scoreTag.innerText)[0], 10) : 0; // Now that we have a score with a numeric value, we could sort a list of rows on that value. // Lets begin by building that collection of rows with their score. // We defined a 'rows' variable above as an empty array []. Now, we'll push arrays to it that contain // the score and an array of the 3 table rows that make up this news headline. // Note, we're indexing into the original elements (not ielements). rows.push([score, [elements[i - 1], elements[i], elements[i + 1]]]); } } // Now we delete the table body with all the table rows in it. document.querySelectorAll(".itemlist tbody")[0].remove(); // The variable we created, tbody, is an HTMLElement living in memory in JS, // it needs to be actually appended to the HTML document so we can see it. // That whole living in memory thing really means its not attached to the // Document Object Model, or DOM. This line brings it into the DOM // by appending it as a child to the element with the class itemlist. // (The same place the last tbody lived before we removed it) document.querySelectorAll(".itemlist")[0].appendChild(tbody); // In some languages, sorting is not a huge pain in the ass. In JS, it just is... // rows.sort((a, b) => {b[0] - a[0]}) // This line is sorting the rows we built on line 92 based on their score values. // Sorting in JS is lexicographic by default and isn't smart enough to figure out // you're giving it numbers, so it will interpret your numbers as strings and // sort them as such by default. So instead we pass a custom compare function in as // an argument to rows.sort. This function is written out here as an es6 arrow function. // These compare functions are supposed to take to elements from the array being // sorted and return a boolean that determines sort order for the elements, or something. // Good one to read up on. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort for ([score, elements] of rows.sort((a, b) => { return b[0] - a[0]; })) { // Okay so we're walking through the sorted news headlines now, each one is a collection of 3 // HTMLElements, so we iterate each of those and append them to our table body. for (let ele of elements) { if (ele) tbody.appendChild(ele); } } // Finally, we iterate over the leftover elements and append them to the tbody. for (let ele of leftovers) { if (ele) tbody.appendChild(ele); }