Search code examples
javascriptjquerydjangosortingtablesorter

Tablesorter sorts customely parsed numbers properly only in descending order, ascending order is wrong


Using Mottie's fork of tablesorter. Needed to sort dates in 'Jan 16' format. From Aug to Jul.

Wrote a custom parser.

let monthsFirstDayIndexes = {
    'Aug': 0,
    'Sep': 31,
    'Oct': 61,
    'Nov': 92,
    'Dec': 122,
    'Jan': 153,
    'Feb': 184,
    'Mar': 213,
    'Apr': 244,
    'May': 274,
    'Jun': 305,
    'Jul': 335,
  }

$.tablesorter.addParser({
  id: 'date',
  is: function() {
    return false;
  },

  format: function(s) {
    dateSplitted = s.split(' ')
    return monthsFirstDayIndexes[dateSplitted[0]] + Number(dateSplitted[1])
 },

     type: 'numeric'
  });

I'm using the parser it the Django template

{% extends 'base.html' %}
{% load static %}
{% load tags %}

{% block title %}
  {{ player.name }} - Game Log - NHL stats tracker
{% endblock title %}

{% block styles %}
  <link rel="stylesheet" href="{% static 'players/tablesorter.css' %}">
{% endblock styles %}

{% block content %}
      <img class="rounded-circle account-img" src="{{ player.image.url }}">
             <h5>{{ player.name }} Game Log</h5>
             <a href="{% url 'player_detail' player.slug player.nhl_id %}">Return to the full profile</a><br>
            <!-- GOALIES       -->
              {% if player.position_abbr == 'G' %}
              <!-- GAMELOG       -->
              <table id="tab1" class="tablesorter">
                <thead>
                  <tr>
                    <th>Date</th>
                    <th>Opp</th>
                    <th>Res</th>
                    <th>Min</th>
                    <th>GA</th>
                    <th>SV %</th>
                    <th>SV</th>
                    <th>SA</th>
                    <th>SHO</th>
                  </tr>
                </thead>
                <!-- PAGER -->
                <tfoot>
                  <tr class="tablesorter-ignoreRow">
                    <th colspan="28" class="pager"></th>
                  </tr>
                  <tr class="tablesorter-ignoreRow">
                    <th colspan="28" class="pager-g form-horizontal pager">
                      <button type="button" class="btn first"><i class="small material-icons">first_page</i></button>
                      <button type="button" class="btn prev"><i class="small material-icons">navigate_before</i></button>
                      <span class="pagedisplay"></span>
                      <button type="button" class="btn next"><i class="small material-icons white">navigate_next</i></button>
                      <button type="button" class="btn last"><i class="small material-icons">last_page</i></button>
                      <select class="pagesize browser-default" title="Select page size">
                        <option selected="selected" value="25">25</option>
                        <option value="50">50</option>
                        <option value="100">100</option>
                        <option value="200">200</option>
                        <option value="all">All</option>
                      </select>
                    </th>
                  </tr>
                </tfoot>
                <tbody>
                {% for game in player.gamelog_stats %}
                  <tr>
                    <td>{{ game.date }}</td>
                    <td>{{ game.opponent.id }}</td>
                    <td>{{ game.stat.decision }}</td>
                    <td>{{ game.stat.timeOnIce }}</td>
                    <td>{{ game.stat.goalsAgainst }}</td>
                    <td>{{ game.stat.savePercentage|floatformat:3 }}</td>
                    <td>{{ game.stat.saves }}</td>
                    <td>{{ game.stat.shotsAgainst }}</td>
                    <td>{{ game.stat.shutouts }}</td>
                  </tr>
                {% endfor %}
                </tbody>
              </table>

              {% else %}
              <!-- SKATERS       -->
              <table id="tab2" class="sortable">
                <thead>
                  <tr>
                    <th class="sorter-date">Date</th>
                    <th>Opp</th>
                    <th>G</th>
                    <th>A</th>
                    <th>Pts</th>
                    <th>+/-</th>
                    <th>PIM</th>
                    <th>SOG</th>
                    <th>Hits</th>
                    <th>Blk</th>
                    <th>FW</th>
                    <th>PPP</th>
                    <th>SHP</th>
                    <th class="sorter-countdown">TOI</th>
                    <th class="sorter-countdown">TOI PP</th>
                    <th class="sorter-countdown">TOI SH</th>
                  </tr>
                </thead>
                <!-- PAGER -->
                <tfoot>
                  <tr class="tablesorter-ignoreRow">
                    <th colspan="28" class="pager"></th>
                  </tr>
                  <tr class="tablesorter-ignoreRow">
                    <th colspan="28" class="pager-s form-horizontal pager">
                      <button type="button" class="btn first"><i class="small material-icons">first_page</i></button>
                      <button type="button" class="btn prev"><i class="small material-icons">navigate_before</i></button>
                      <span class="pagedisplay"></span>
                      <button type="button" class="btn next"><i class="small material-icons white">navigate_next</i></button>
                      <button type="button" class="btn last"><i class="small material-icons">last_page</i></button>
                      <select class="pagesize browser-default" title="Select page size">
                        <option selected="selected" value="25">25</option>
                        <option value="50">50</option>
                        <option value="100">100</option>
                        <option value="200">200</option>
                        <option value="all">All</option>
                      </select>
                    </th>
                  </tr>
                </tfoot>
                <tbody>
                {% for game in player.gamelog_stats %}
                  <tr>
                    <td>{{ game.date }}</td>
                    <td>{{ game.opponent.id }}</td>
                    <td>{{ game.stat.goals }}</td>
                    <td>{{ game.stat.assists }}</td>
                    <td>{{ game.stat.points }}</td>
                    <td>{{ game.stat.plusMinus }}</td>
                    <td>{{ game.stat.pim }}</td>
                    <td>{{ game.stat.shots }}</td>
                    <td>{{ game.stat.hits }}</td>
                    <td>{{ game.stat.blocked }}</td>
                    <td>{{ game.stat.faceOffWins }}</td>
                    <td>{{ game.stat.powerPlayPoints }}</td>
                    <td>{{ game.stat.shortHandedPoints }}</td>
                    <td>{{ game.stat.timeOnIce }}</td>
                    <td>{{ game.stat.powerPlayTimeOnIce }}</td>
                    <td>{{ game.stat.shortHandedTimeOnIce }}</td>
                  </tr>
                {% endfor %}
                </tbody>
              </table>
              {% endif %}
{% endblock content %}

{% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.1/js/jquery.tablesorter.js"></script>
<!-- Widgets -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.1/js/jquery.tablesorter.widgets.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.1/js/extras/jquery.tablesorter.pager.min.js"></script>

<script src="{% static 'players/parser-duration.js' %}"></script>
<script src="{% static 'players/parser-date.js' %}"></script>
<script src="{% static 'players/sorting_player_gamelog.js' %}"></script>
{% endblock scripts %}

The result of sorting in asc order. The problem is that dates with days < 10 are sorted in the wrong order.

enter image description here

The desc order is ok.

enter image description here

Also tried to use parseInt(dateSplitted[1]) instead of Number(dateSplitted[1]). It's produced even stranger results.

Tested the same parsing method in plain JS, with custom sorting. It works.

let monthsFirstDayIndexes = {
    'Aug': 0,
    'Sep': 31,
    'Oct': 61,
    'Nov': 92,
    'Dec': 122,
    'Jan': 153,
    'Feb': 184,
    'Mar': 213,
    'Apr': 244,
    'May': 274,
    'Jun': 305,
    'Jul': 335,
  }

let dates = [
  'Mar 3',
  'Nov 24',
  'Nov 25',
  'Sep 14',
  'Mar 5',
  'Mar 7',
  'Jan 25',
  'Sep 8',
  'Mar 16',
  'Jan 12',
  'Mar 8',
]

dateSplitted = dates.map(item => item.split(' '));

function sortFunc(a, b) {
  return (monthsFirstDayIndexes[a[0]] + Number(a[1])) - (monthsFirstDayIndexes[b[0]] + Number(b[1]));
}

console.log(dateSplitted.sort(sortFunc).map(item => item.join(' ')));

Any thoughts of how to make it work for tablesorter? The thing is I don't really know how to debug my code in this case, how to see the actual numbers it's parsing from the dates in the html code.


Solution

  • The problem was actually not in the JavaScript. I got confused because on the page all dates shown without extra spaces between symbols. But there was an extra space in all dates where day < 10. I caught that checking the HTML source code.

    enter image description here

    There was a bug in Django script that loads and converts date format from an API source to my DB.

    def date_convert(date):
        datetime_obj = datetime.datetime.strptime(date, '%Y-%m-%d')
        return datetime_obj.strftime('%b %e')
    

    I've changed the day format from %d(zero padded) to %e(no zero paddings). So it was adding extra space to 1-digit days instead of a zero. And Number(dateSplitted[1] was equal to zero, not the day number I've expected. So all of the dates with 1-digit days were sorted like monthsFirstDayIndexes for a given month, without actual day numbers added.

    So I need to use a zero padding or remove the excessive space. I decided to go with the second option. Used regex

    import re 
    
    def date_convert(date):
        datetime_obj = datetime.datetime.strptime(date, '%Y-%m-%d')
        date_str = datetime_obj.strftime('%b %e')
        return re.sub(r'\s+', ' ', date_str)
    

    I also would like to add that I could've easily found the source of the error if I knew that I could just log anything I want to the browser console (F12 in Firefox for instance). The picture clearly shows that there is an extra space in some dates and because of it parser did not produce numbers I've expected. This knowledge could save you some time. The console.log name is kinda obvious in this case by the way.

    enter image description here.