Search code examples
javascripthtmlsorting

Sorting a HTML table


I have a HTML table that contains several columns. I've found a JS w3school snippet to sort this table. I've slightly modified this script to be able to sort also by a column that contains only a number. It works well except for 1 column that contains values like this 548m / 1797ft. This kind of value go after higher one?! I really don't understand what's going wrong for this particular column. Code:

function sortTable(n) {
  var table,
    rows,
    switching,
    i,
    x,
    y,
    shouldSwitch,
    dir,
    switchcount = 0;
  table = document.getElementById("table");
  switching = true;
  // Set the sorting direction to ascending:
  dir = "asc";
  /* Make a loop that will continue until
    no switching has been done: */
  while (switching) {
    // Start by saying: no switching is done:
    switching = false;
    rows = table.rows;
    /* Loop through all table rows (except the
      first, which contains table headers): */
    for (i = 1; i < rows.length - 1; i++) {
      // Start by saying there should be no switching:
      shouldSwitch = false;
      /* Get the two elements you want to compare,
        one from current row and one from the next: */
      x = rows[i].getElementsByTagName("TD")[n];
      y = rows[i + 1].getElementsByTagName("TD")[n];
      /* Check if the two rows should switch place,
        based on the direction, asc or desc: */
      if (!isNaN(x.innerHTML)) {
        // NUMERIC
        if (dir == "asc") {
          if (Number(x.innerHTML) > Number(y.innerHTML)) {
            shouldSwitch = true;
            break;
          }
        } else if (dir == "desc") {
          if (Number(x.innerHTML) < Number(y.innerHTML)) {
            shouldSwitch = true;
            break;
          }
        }
      } else {
        // ALPHABETIC
        if (dir == "asc") {
          if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
            // If so, mark as a switch and break the loop:
            shouldSwitch = true;
            break;
          }
        } else if (dir == "desc") {
          if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
            // If so, mark as a switch and break the loop:
            shouldSwitch = true;
            break;
          }
        }
      }
    } // FOR LOOP

    if (shouldSwitch) {
      /* If a switch has been marked, make the switch
        and mark that a switch has been done: */
      rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
      switching = true;
      // Each time a switch is done, increase this count by 1:
      switchcount++;
    } else {
      /* If no switching has been done AND the direction is "asc",
        set the direction to "desc" and run the while loop again. */
      if (switchcount == 0 && dir == "asc") {
        dir = "desc";
        switching = true;
      }
    }
  }
}
<table id="table" class="resp">
  <thead>
    <tr>
      <th onclick="sortTable(0)" class="sortable" scope="col">Position</th>
      <th onclick="sortTable(1)" class="sortable" scope="col">Sommet</th>
      <th onclick="sortTable(2)" class="sortable" scope="col">Altitude</th>
    </tr>
  </thead>
  <tr>
    <td data-label="Position">1</td>
    <td data-label="Sommet">Mont Marcy</td>
    <td data-label="Altitude">1629m / 5343ft</td>
  </tr>
  <tr>
    <td data-label="Position">2</td>
    <td data-label="Sommet">Mont Algonquin</td>
    <td data-label="Altitude">1559m / 5114ft</td>
  </tr>
  <tr>
    <td data-label="Position">3</td>
    <td data-label="Sommet">Mont Haystack</td>
    <td data-label="Altitude">1510m / 4953ft</td>
  </tr>
  <tr>
    <td data-label="Position">4</td>
    <td data-label="Sommet">Mont Skylight</td>
    <td data-label="Altitude">1501m / 4923ft</td>
  </tr>
  <tr>
    <td data-label="Position">5</td>
    <td data-label="Sommet">Mont Whiteface</td>
    <td data-label="Altitude">1483m / 4864ft</td>
  </tr>
  <tr>
    <td data-label="Position">6</td>
    <td data-label="Sommet">Mont Dix</td>
    <td data-label="Altitude">1481m / 4858ft</td>
  </tr>
  <tr>
    <td data-label="Position">7</td>
    <td data-label="Sommet">Mont Gray</td>
    <td data-label="Altitude">1475m / 4838ft</td>
  </tr>
  <tr>
    <td data-label="Position">8</td>
    <td data-label="Sommet">Mont Iroquois</td>
    <td data-label="Altitude">1475m / 4838ft</td>
  </tr>
  <tr>
    <td data-label="Position">9</td>
    <td data-label="Sommet">Mont Iroquois</td>
    <td data-label="Altitude">1001m / 3283ft</td>
  </tr>
  <tr>
    <td data-label="Position">10</td>
    <td data-label="Sommet">Mont St-Bruno</td>
    <td data-label="Altitude">548m / 1797ft</td>
  </tr>
  <tr>
    <td data-label="Position">11</td>
    <td data-label="Sommet">Mont Royal</td>
    <td data-label="Altitude">472m / 1548ft</td>
  </tr>
</table>

I would think that this particular column would be treated like a string, but it seems not... So, I really don't know what to do more.


Solution

  • Some remarks on your attempt:

    1. You can use what is called natural sort. This would also sort things like "a13b8c", "a13b50", "a2" in that order, and it doesn't require you to first check whether the data are numbers or not: it works in either case.
    2. There is no need to implement a bubble sort algorithm; you can use the native sort function
    3. Don't use innerHTML, as that might give you HTML entities like &nbsp; or &lt;. Instead use textContent or innerText.
    4. You should trim the values before comparing
    5. rows[i].getElementsByTagName("TD")[n]; is really a verbose way to do rows[i].cells[n].

    If you apply the above suggestions, the code can be reduced to only a few lines. I didn't touch your HTML (except indentation/whitespace):

    function sortTable(col) {
        // Prepare for performing a "natural" sort
        const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
        // Get the rows that should be sorted, excluding the header/footer rows
        const rows = table.querySelectorAll("#table tr:not(thead>tr):not(tfoot>tr)");
        // Get the container element for these rows (tbody):
        const parent = rows?.[0]?.parentNode;
        // Get the texts of the selected column in combination with the row indices and TR elements
        const triplets = Array.from(rows, (row, i) => [row.cells[col].textContent, i, row])
                         // ... and apply natural sort on the texts
                        .sort(([a], [b]) => collator.compare(a.trim(), b.trim()));
        // If sorting didn't change anything then reverse:
        if (triplets.every(([,i], j) => i === j)) triplets.reverse();
        // Repopulate the table rows in their new order
        for (const [,, row] of triplets) parent.appendChild(row);
    }
    <TABLE id="table" class="resp">
        <thead>
            <TR>
                <th onclick="sortTable(0)" class="sortable" scope="col"> Position </th>
                <th onclick="sortTable(1)" class="sortable" scope="col"> Sommet </th>
                <th onclick="sortTable(2)" class="sortable" scope="col"> Altitude </th>
            </TR>
        </thead>
        <tr>
            <td data-label="Position"> 1 </td>
            <td data-label="Sommet"> Mont Marcy </td>
            <td data-label="Altitude"> 1629m / 5343ft </td>
        </tr>
        <tr>
            <td data-label="Position"> 2 </td>
            <td data-label="Sommet"> Mont Algonquin </td>
            <td data-label="Altitude"> 1559m / 5114ft </td>                      
        </tr>
        <tr>
            <td data-label="Position"> 3 </td>
            <td data-label="Sommet"> Mont Haystack </td>
            <td data-label="Altitude"> 1510m / 4953ft </td>                         
        </tr>
        <tr>
            <td data-label="Position"> 4 </td>
            <td data-label="Sommet"> Mont Skylight </td>
            <td data-label="Altitude"> 1501m / 4923ft </td>
        </tr>
        <tr>
            <td data-label="Position"> 5 </td>
            <td data-label="Sommet"> Mont Whiteface</td>
            <td data-label="Altitude">1483m / 4864ft </td>
        </tr>
        <tr>
            <td data-label="Position"> 6 </td>
            <td data-label="Sommet"> Mont Dix </td>
            <td data-label="Altitude"> 1481m / 4858ft </td> 
        </tr>
        <tr>
            <td data-label="Position"> 7  </td>
            <td data-label="Sommet"> Mont Gray </td>
            <td data-label="Altitude"> 1475m / 4838ft </td> 
        </tr>
        <tr>
            <td data-label="Position"> 8 </td>
            <td data-label="Sommet"> Mont Iroquois </td>
            <td data-label="Altitude"> 1475m / 4838ft </td> 
        </tr>
        <tr>
            <td data-label="Position"> 9 </td>
            <td data-label="Sommet"> Mont Iroquois </td>
            <td data-label="Altitude"> 1001m / 3283ft </td> 
        </tr>
        <tr>
            <td data-label="Position"> 10 </td>
            <td data-label="Sommet"> Mont St-Bruno </td>
            <td data-label="Altitude"> 548m / 1797ft </td>
        </tr>
        <tr>
            <td data-label="Position"> 11 </td>
            <td data-label="Sommet"> Mont Royal </td>
            <td data-label="Altitude"> 472m / 1548ft </td>
        </tr>
    </table>