57
\$\begingroup\$

I've been using a stack snippet for a while to generate leaderboards for my simpler or more popular code golf challenges. I want to share the code for this snippet here so others can use it more easily as well.

Feel free to put feature requests and bug reports in answers, or let me know in chat.

I'm providing both the minified code (for use in your challenges) and the unminified code (for reviewing/improving the code). Both snippets in this post point to Shotgun Numbers. At the bottom you'll also find the blurb I put in my challenges to explain the header formatting.

Features and Usage

  • Shows two leaderboards: one overall leaderboard listing users with their languages by score; and a list of languages used with the best score (and user) per language. The latter is sorted by languages, not by scores (because the language ranking by scores can be read off the main leaderboard).
  • Correctly handles ties in the main leaderboard (listing, e.g. 1. 2. 2. 4. ...).
  • It takes the last number that is not struck out as the score. This allows keeping previous scores in the header (anywhere) as well as presenting partial scores like Python 2, 34 + 71 = 105.
  • Links for language names are allowed and show up in the leaderboard. For the by-language leaderboard, HTML is stripped when comparing languages for equality.
  • One user can be chosen (via the OVERRIDE_USER ID in the code) who can override any headers using a comment. This user will usually be the author of the challenge. The comment has to start with Override header: - anything after that will be treated as the new header. If you want to exclude an answer from the leaderboard, just use something like Override header: invalid. This feature is only intended as a last resort to avoid edit wars if some user refuses to use a correct header or delete an invalid answer.

To use the leaderboard in your own challenge, simply copy the snippet and search for the global variable definition of QUESTION_ID and replace it with the ID found in the URL to your question and replace OVERRIDE_USER with your own user ID.

Minified Code

var QUESTION_ID=47338;
var OVERRIDE_USER=8478;
var ANSWER_FILTER="!t)IWYnsLAZle2tQ3KqrVveCRJfxcRLe",COMMENT_FILTER="!)Q2B_A2kjfAiU78X(md6BoYk",answers=[],answers_hash,answer_ids,answer_page=1,more_answers=!0,comment_page;function answersUrl(d){return"https://api.stackexchange.com/2.2/questions/"+QUESTION_ID+"/answers?page="+d+"&pagesize=100&order=desc&sort=creation&site=codegolf&filter="+ANSWER_FILTER}function commentUrl(d,e){return"https://api.stackexchange.com/2.2/answers/"+e.join(";")+"/comments?page="+d+"&pagesize=100&order=desc&sort=creation&site=codegolf&filter="+COMMENT_FILTER}function getAnswers(){jQuery.ajax({url:answersUrl(answer_page++),method:"get",dataType:"jsonp",crossDomain:!0,success:function(d){answers.push.apply(answers,d.items),answers_hash=[],answer_ids=[],d.items.forEach(function(e){e.comments=[];var f=+e.share_link.match(/\d+/);answer_ids.push(f),answers_hash[f]=e}),d.has_more||(more_answers=!1),comment_page=1,getComments()}})}function getComments(){jQuery.ajax({url:commentUrl(comment_page++,answer_ids),method:"get",dataType:"jsonp",crossDomain:!0,success:function(d){d.items.forEach(function(e){e.owner.user_id===OVERRIDE_USER&&answers_hash[e.post_id].comments.push(e)}),d.has_more?getComments():more_answers?getAnswers():process()}})}getAnswers();var SCORE_REG=function(){var d=String.raw`h\d`,e=String.raw`\-?\d+\.?\d*`,f=String.raw`[^\n<>]*`,g=String.raw`<s>${f}</s>|<strike>${f}</strike>|<del>${f}</del>`,h=String.raw`[^\n\d<>]*`,j=String.raw`<[^\n<>]+>`;return new RegExp(String.raw`<${d}>`+String.raw`\s*([^\n,]*[^\s,]),.*?`+String.raw`(${e})`+String.raw`(?=`+String.raw`${h}`+String.raw`(?:(?:${g}|${j})${h})*`+String.raw`</${d}>`+String.raw`)`)}(),OVERRIDE_REG=/^Override\s*header:\s*/i;function getAuthorName(d){return d.owner.display_name}function process(){var d=[];answers.forEach(function(n){var o=n.body;n.comments.forEach(function(q){OVERRIDE_REG.test(q.body)&&(o="<h1>"+q.body.replace(OVERRIDE_REG,"")+"</h1>")});var p=o.match(SCORE_REG);p&&d.push({user:getAuthorName(n),size:+p[2],language:p[1],link:n.share_link})}),d.sort(function(n,o){var p=n.size,q=o.size;return p-q});var e={},f=1,g=null,h=1;d.forEach(function(n){n.size!=g&&(h=f),g=n.size,++f;var o=jQuery("#answer-template").html();o=o.replace("{{PLACE}}",h+".").replace("{{NAME}}",n.user).replace("{{LANGUAGE}}",n.language).replace("{{SIZE}}",n.size).replace("{{LINK}}",n.link),o=jQuery(o),jQuery("#answers").append(o);var p=n.language;p=jQuery("<i>"+n.language+"</i>").text().toLowerCase(),e[p]=e[p]||{lang:n.language,user:n.user,size:n.size,link:n.link,uniq:p}});var j=[];for(var k in e)e.hasOwnProperty(k)&&j.push(e[k]);j.sort(function(n,o){return n.uniq>o.uniq?1:n.uniq<o.uniq?-1:0});for(var l=0;l<j.length;++l){var m=jQuery("#language-template").html(),k=j[l];m=m.replace("{{LANGUAGE}}",k.lang).replace("{{NAME}}",k.user).replace("{{SIZE}}",k.size).replace("{{LINK}}",k.link),m=jQuery(m),jQuery("#languages").append(m)}}
body{text-align:left!important}#answer-list{padding:10px;float:left}#language-list{padding:10px;float:left}table thead{font-weight:700}table td{padding:5px}
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <link rel="stylesheet" type="text/css" href="https://cdn.sstatic.net/Sites/codegolf/primary.css?v=f52df912b654"> <div id="language-list"> <h2>Winners by Language</h2> <table class="language-list"> <thead> <tr><td>Language</td><td>User</td><td>Score</td></tr></thead> <tbody id="languages"> </tbody> </table> </div><div id="answer-list"> <h2>Leaderboard</h2> <table class="answer-list"> <thead> <tr><td></td><td>Author</td><td>Language</td><td>Size</td></tr></thead> <tbody id="answers"> </tbody> </table> </div><table style="display: none"> <tbody id="answer-template"> <tr><td>{{PLACE}}</td><td>{{NAME}}</td><td>{{LANGUAGE}}</td><td><a href="{{LINK}}">{{SIZE}}</a></td></tr></tbody> </table> <table style="display: none"> <tbody id="language-template"> <tr><td>{{LANGUAGE}}</td><td>{{NAME}}</td><td><a href="{{LINK}}">{{SIZE}}</a></td></tr></tbody> </table> 

Unminified Code

/* Configuration */

var QUESTION_ID = 47338; // Obtain this from the url
// It will be like https://XYZ.stackexchange.com/questions/QUESTION_ID/... on any question page
var ANSWER_FILTER = "!t)IWYnsLAZle2tQ3KqrVveCRJfxcRLe";
var COMMENT_FILTER = "!)Q2B_A2kjfAiU78X(md6BoYk";
var OVERRIDE_USER = 8478; // This should be the user ID of the challenge author.

/* App */

var answers = [], answers_hash, answer_ids, answer_page = 1, more_answers = true, comment_page;

function answersUrl(index) {
  return "https://api.stackexchange.com/2.2/questions/" +  QUESTION_ID + "/answers?page=" + index + "&pagesize=100&order=desc&sort=creation&site=codegolf&filter=" + ANSWER_FILTER;
}

function commentUrl(index, answers) {
  return "https://api.stackexchange.com/2.2/answers/" + answers.join(';') + "/comments?page=" + index + "&pagesize=100&order=desc&sort=creation&site=codegolf&filter=" + COMMENT_FILTER;
}

function getAnswers() {
  jQuery.ajax({
    url: answersUrl(answer_page++),
    method: "get",
    dataType: "jsonp",
    crossDomain: true,
    success: function (data) {
      answers.push.apply(answers, data.items);
      answers_hash = [];
      answer_ids = [];
      data.items.forEach(function(a) {
        a.comments = [];
        var id = +a.share_link.match(/\d+/);
        answer_ids.push(id);
        answers_hash[id] = a;
      });
      if (!data.has_more) more_answers = false;
      comment_page = 1;
      getComments();
    }
  });
}

function getComments() {
  jQuery.ajax({
    url: commentUrl(comment_page++, answer_ids),
    method: "get",
    dataType: "jsonp",
    crossDomain: true,
    success: function (data) {
      data.items.forEach(function(c) {
        if (c.owner.user_id === OVERRIDE_USER)
          answers_hash[c.post_id].comments.push(c);
      });
      if (data.has_more) getComments();
      else if (more_answers) getAnswers();
      else process();
    }
  });  
}

getAnswers();

var SCORE_REG = (function(){
  var headerTag     = String.raw `h\d`
  var score         = String.raw `\-?\d+\.?\d*` // with negative/floating-point support
  var normalText    = String.raw `[^\n<>]*` // no HTML tag, no newline
  var strikethrough = String.raw `<s>${normalText}</s>|<strike>${normalText}</strike>|<del>${normalText}</del>`
  var noDigitText   = String.raw `[^\n\d<>]*`
  var htmlTag       = String.raw `<[^\n<>]+>`

  return new RegExp(
  String.raw  `<${headerTag}>`+
  String.raw    `\s*([^\n,]*[^\s,]),.*?`+
  String.raw    `(${score})`+
  String.raw    `(?=`+
  String.raw      `${noDigitText}`+
  String.raw      `(?:(?:${strikethrough}|${htmlTag})${noDigitText})*`+
  String.raw      `</${headerTag}>`+
  String.raw    `)`
  );
})();

var OVERRIDE_REG = /^Override\s*header:\s*/i;

function getAuthorName(a) {
  return a.owner.display_name;
}

function process() {
  var valid = [];
  
  answers.forEach(function(a) {
    var body = a.body;
    a.comments.forEach(function(c) {
      if(OVERRIDE_REG.test(c.body))
        body = '<h1>' + c.body.replace(OVERRIDE_REG, '') + '</h1>';
    });
    
    var match = body.match(SCORE_REG);
    if (match)
      valid.push({
        user: getAuthorName(a),
        size: +match[2],
        language: match[1],
        link: a.share_link,
      });
    
  });
  
  valid.sort(function (a, b) {
    var aB = a.size,
        bB = b.size;
    return aB - bB
  });

  var languages = {};
  var place = 1;
  var lastSize = null;
  var lastPlace = 1;
  valid.forEach(function (a) {
    if (a.size != lastSize)
      lastPlace = place;
    lastSize = a.size;
    ++place;
    
    var answer = jQuery("#answer-template").html();
    answer = answer.replace("{{PLACE}}", lastPlace + ".")
                   .replace("{{NAME}}", a.user)
                   .replace("{{LANGUAGE}}", a.language)
                   .replace("{{SIZE}}", a.size)
                   .replace("{{LINK}}", a.link);
    answer = jQuery(answer);
    jQuery("#answers").append(answer);

    var lang = a.language;
    lang = jQuery('<i>' + a.language + '</i>').text().toLowerCase();
    
    languages[lang] = languages[lang] || {lang: a.language, user: a.user, size: a.size, link: a.link, uniq: lang};
  });

  var langs = [];
  for (var lang in languages)
    if (languages.hasOwnProperty(lang))
      langs.push(languages[lang]);

  langs.sort(function (a, b) {
    if (a.uniq > b.uniq) return 1;
    if (a.uniq < b.uniq) return -1;
    return 0;
  });

  for (var i = 0; i < langs.length; ++i)
  {
    var language = jQuery("#language-template").html();
    var lang = langs[i];
    language = language.replace("{{LANGUAGE}}", lang.lang)
                       .replace("{{NAME}}", lang.user)
                       .replace("{{SIZE}}", lang.size)
                       .replace("{{LINK}}", lang.link);
    language = jQuery(language);
    jQuery("#languages").append(language);
  }

}
body { text-align: left !important}

#answer-list {
  padding: 10px;
  float: left;
}

#language-list {
  padding: 10px;
  float: left;
}

table thead {
  font-weight: bold;
}

table td {
  padding: 5px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.sstatic.net/Sites/codegolf/primary.css?v=f52df912b654">
<div id="language-list">
  <h2>Winners by Language</h2>
  <table class="language-list">
    <thead>
      <tr><td>Language</td><td>User</td><td>Score</td></tr>
    </thead>
    <tbody id="languages">

    </tbody>
  </table>
</div>
<div id="answer-list">
  <h2>Leaderboard</h2>
  <table class="answer-list">
    <thead>
      <tr><td></td><td>Author</td><td>Language</td><td>Size</td></tr>
    </thead>
    <tbody id="answers">

    </tbody>
  </table>
</div>

<table style="display: none">
  <tbody id="answer-template">
    <tr><td>{{PLACE}}</td><td>{{NAME}}</td><td>{{LANGUAGE}}</td><td><a href="{{LINK}}">{{SIZE}}</a></td></tr>
  </tbody>
</table>
<table style="display: none">
  <tbody id="language-template">
    <tr><td>{{LANGUAGE}}</td><td>{{NAME}}</td><td><a href="{{LINK}}">{{SIZE}}</a></td></tr>
  </tbody>
</table>

Info Section

You can copy the following Markdown into your challenge as instructions for your participants.

## Leaderboards

Here is a Stack Snippet to generate both a regular leaderboard and an overview of winners by language.

To make sure that your answer shows up, please start your answer with a headline, using the following Markdown template:

    # Language Name, N bytes

where `N` is the size of your submission. If you improve your score, you *can* keep old scores in the headline, by striking them through. For instance:

    # Ruby, <s>104</s> <s>101</s> 96 bytes

If there you want to include multiple numbers in your header (e.g. because your score is the sum of two files or you want to list interpreter flag penalties separately), make sure that the actual score is the *last* number in the header:

    # Perl, 43 + 2 (-p flag) = 45 bytes

You can also make the language name a link which will then show up in the leaderboard snippet:

    # [><>](http://esolangs.org/wiki/Fish), 121 bytes
\$\endgroup\$
  • \$\begingroup\$ It looks like ANSWER_FILTER isn't defined in the minified code, but it is in the unminified code. (The minified code still works though.) Also, what is it? \$\endgroup\$ – Alex A. Apr 24 '15 at 21:33
  • 1
    \$\begingroup\$ Is there a reason why you scrape the Markdown to get the header, instead of parsing the generated HTML? \$\endgroup\$ – NinjaBearMonkey Apr 24 '15 at 21:53
  • \$\begingroup\$ @AlexA. The minifier moves definitions around, and ANSWER_FILTER is somewhere at the end. (In fact, QUESTION_ID also ended up there, but I moved that back to the front so it would be easier to find.) \$\endgroup\$ – Martin Ender Apr 24 '15 at 22:20
  • \$\begingroup\$ @NinjaBearMonkey That seemed easier somehow. I guess I could look into using jQuery to fetch the score from the HTML. \$\endgroup\$ – Martin Ender Apr 24 '15 at 22:23
  • \$\begingroup\$ @AlexA. Oh forgot to answer your second question. It's a token sent to the Stack Exchange API which tells the API which fields I want populated in the JSON response (like user ID, timestamp, markdown body, HTML body, etc.). You can create your own here. \$\endgroup\$ – Martin Ender Apr 24 '15 at 22:28
  • \$\begingroup\$ @NinjaBearMonkey Reading the markdown is arguably better because you can look for code blocks with syntax highlighting. HTML mangles highlighted code. On the other hand, not all 4-space indented text is code in markdown, so the regex to match markdown code blocks is trickier if you want to do it right. \$\endgroup\$ – Calvin's Hobbies Apr 27 '15 at 3:00
  • \$\begingroup\$ @Calvin'sHobbies That's a good point. I guess there's a trade-off between syntax highlighting and getting the code easily and reliably. \$\endgroup\$ – NinjaBearMonkey Apr 27 '15 at 11:56
  • \$\begingroup\$ @Calvin'sHobbies Then again you can also uses <code><pre> in markdown to get a code block. \$\endgroup\$ – Martin Ender Apr 27 '15 at 11:57
  • \$\begingroup\$ Does it handle <del> instead of <s>? \$\endgroup\$ – Ismael Miguel May 3 '15 at 21:14
  • 1
    \$\begingroup\$ @IsmaelMiguel Not really. I don't think I will fix this before changing the entire parsing from regex to jQuery though. No point in patching loads of special cases into that horrible regex if I could just do it right. Also, who would want to type del instead of s anyway? We're still on CodeGolf.SE after all! :P \$\endgroup\$ – Martin Ender May 3 '15 at 21:19
  • \$\begingroup\$ @MartinBüttner Yes, we are. But <del> is the right tag to use. Since an element was removed (in this case, the old byte count). Visually, it makes no difference. \$\endgroup\$ – Ismael Miguel May 3 '15 at 21:27
  • \$\begingroup\$ Can the \d+ be changed to \-?\d+ so it accepts negative numbers (for bonuses and such)? \$\endgroup\$ – Daniel M. Oct 3 '15 at 0:09
  • \$\begingroup\$ Can you please replace every occurrence of http://api.stackexchange.com/ with //api.stackexchange.com/ (a protocol-relative URL) so the leaderboard works on https://codegolf.stackexchange.com without having to disable mixed content blocking every time? \$\endgroup\$ – user2428118 Oct 3 '15 at 16:16
  • \$\begingroup\$ @MartinBüttner would you have any objection to the minified snippet markdown being uploaded to pastebin so it can be copied and pasted into posts in a single step? I tried making an answer with the raw markdown showing but <!-- seems to prevent it. \$\endgroup\$ – trichoplax Nov 4 '15 at 11:24
  • 3
    \$\begingroup\$ @Nathaniel A quick fix would be to change the \d+ in that long regex to \d+(?:\.\d+) but I can't tell you off the top of my head how robust that will be. \$\endgroup\$ – Martin Ender Jan 24 '16 at 13:41

11 Answers 11

11
\$\begingroup\$

Feature request: Column rearrangement

In my opinion, the per-language results should be on the left since we emphasize competition within each language. It would simply be a swapping of the current left and right columns.

\$\endgroup\$
  • 2
    \$\begingroup\$ I'd put together a prototype but I'm not good with computer \$\endgroup\$ – Alex A. May 25 '16 at 20:41
  • 2
    \$\begingroup\$ Once we see vote support I'm sure there are lots of people who can make the change. If no one volunteers I need some practice myself. \$\endgroup\$ – trichoplax May 25 '16 at 20:42
  • \$\begingroup\$ status-complete. \$\endgroup\$ – user202729 May 28 '18 at 13:12
7
\$\begingroup\$

Layout problem

I'm seeing overlapping of text in some cases. For example, this challenge.

Overlapped section of leaderboard

The Winners by Language section starts too far to the left, overlapping the Link from the Leaderboard section.

I'm not sure why this only happens sometimes. Two potential solutions:

  1. Shift the Winners by Language section to the right.
  2. Use manatwork's suggestion of making the size a link to the solution.

I don't see any ambiguity in making the size a link, as I can't imagine anything else it would link to. It does mean the link is no longer explicit though. Does anyone have any other arguments either way?


Update

Although I am still in favour of making the size a link to the solution, Martin pointed out that this doesn't solve the problem. The Winners by Language section is at a fixed horizontal position, so if the Leaderboard section is too wide due to a long author or language name, there could still be overlap even without a separate link column.

So ideally instead of just shifting the Winners by Language right a bit, it needs to be sensitive to the width of the Leaderboard section. I propose work on this be delayed until after the two sections swap sides, once that proposal gets enough votes.

\$\endgroup\$
  • \$\begingroup\$ The leaderboard script in the challenge you linked tries to include the old stylesheet that is not available anymore since PPCG graduated. I would say, that should be changed in first place. \$\endgroup\$ – manatwork May 26 '16 at 7:36
  • \$\begingroup\$ @manatwork I've upvoted your suggestion of a stylesheet redirect to deal with future changes. While we wait to see if that will happen, I guess we just need to edit the snippet to reflect the current stylesheet so it works for now. \$\endgroup\$ – trichoplax May 26 '16 at 11:45
  • \$\begingroup\$ status-complete. \$\endgroup\$ – user202729 May 28 '18 at 13:48
6
\$\begingroup\$

Bug

Although the documentation says that strikeout is supported, it only supports <s> and not <strike> or <del>.

\$\endgroup\$
  • 1
    \$\begingroup\$ @MartinEnder any confirmation, disconfirmation, or bug fix down the pipeline? \$\endgroup\$ – noɥʇʎԀʎzɐɹƆ Jun 22 '16 at 19:21
  • \$\begingroup\$ Done. (MartinEnder told me that I can edit the post here) \$\endgroup\$ – user202729 May 31 '18 at 4:56
6
\$\begingroup\$

I would like to suggest a couple of minor changes. In order of appearance:

  • JavaScript

    • Preserve sign of size by adding -? to the size's group in SCORE_REG (ex: “3” vs. “-3”)

      var SCORE_REG = /<h\d>\s*([^\n,]*[^\s,]),.*?(-?\d+)(?=[^\n\d<>]*(?:<(?:s>[^\n<>]*<\/s>|[^\n<>]+>)[^\n\d<>]*)*<\/h\d>)/;
      
    • HTML-ify plain text language name, so can jQuery()-fy it and get rid of SGML character entities when getting its text() (eg: “><>” vs. “>&lt;>”)

      if (! /<a/.test(lang)) lang = '<i>' + lang + '</i>';
      
    • Transform the langage name in toLowerCase() (eg: “JavaScript” vs. “Javascript”)

      lang = jQuery(lang).text().toLowerCase();
      
    • Keep the stripped language with answer data

      languages[lang] = languages[lang] || {lang: a.language, user: a.user, size: a.size, link: a.link, uniq: lang};
      
    • Sort by the stripped language name

      if (a.uniq > b.uniq) return 1;
      if (a.uniq < b.uniq) return -1;
      
  • HTML

    • Change the URL of stylesheet to the graduated one

      <link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/Sites/codegolf/primary.css?v=f52df912b654">
      
    • Use the score as link text (eg: “3” vs. “3 Link”)

      <a href="{{LINK}}">{{SIZE}}</a>
      

Suggested changes applied to the following snippet, configured to show Create output twice the length of the code's score:

/* Configuration */

var QUESTION_ID = 59436; // Obtain this from the url
// It will be like https://XYZ.stackexchange.com/questions/QUESTION_ID/... on any question page
var ANSWER_FILTER = "!t)IWYnsLAZle2tQ3KqrVveCRJfxcRLe";
var COMMENT_FILTER = "!)Q2B_A2kjfAiU78X(md6BoYk";
var OVERRIDE_USER = 41505; // This should be the user ID of the challenge author.

/* App */

var answers = [], answers_hash, answer_ids, answer_page = 1, more_answers = true, comment_page;

function answersUrl(index) {
  return "https://api.stackexchange.com/2.2/questions/" +  QUESTION_ID + "/answers?page=" + index + "&pagesize=100&order=desc&sort=creation&site=codegolf&filter=" + ANSWER_FILTER;
}

function commentUrl(index, answers) {
  return "https://api.stackexchange.com/2.2/answers/" + answers.join(';') + "/comments?page=" + index + "&pagesize=100&order=desc&sort=creation&site=codegolf&filter=" + COMMENT_FILTER;
}

function getAnswers() {
  jQuery.ajax({
    url: answersUrl(answer_page++),
    method: "get",
    dataType: "jsonp",
    crossDomain: true,
    success: function (data) {
      answers.push.apply(answers, data.items);
      answers_hash = [];
      answer_ids = [];
      data.items.forEach(function(a) {
        a.comments = [];
        var id = +a.share_link.match(/\d+/);
        answer_ids.push(id);
        answers_hash[id] = a;
      });
      if (!data.has_more) more_answers = false;
      comment_page = 1;
      getComments();
    }
  });
}

function getComments() {
  jQuery.ajax({
    url: commentUrl(comment_page++, answer_ids),
    method: "get",
    dataType: "jsonp",
    crossDomain: true,
    success: function (data) {
      data.items.forEach(function(c) {
        if (c.owner.user_id === OVERRIDE_USER)
          answers_hash[c.post_id].comments.push(c);
      });
      if (data.has_more) getComments();
      else if (more_answers) getAnswers();
      else process();
    }
  });  
}

getAnswers();

var SCORE_REG = /<h\d>\s*([^\n,]*[^\s,]),.*?(-?\d+)(?=[^\n\d<>]*(?:<(?:s>[^\n<>]*<\/s>|[^\n<>]+>)[^\n\d<>]*)*<\/h\d>)/;

var OVERRIDE_REG = /^Override\s*header:\s*/i;

function getAuthorName(a) {
  return a.owner.display_name;
}

function process() {
  var valid = [];
  
  answers.forEach(function(a) {
    var body = a.body;
    a.comments.forEach(function(c) {
      if(OVERRIDE_REG.test(c.body))
        body = '<h1>' + c.body.replace(OVERRIDE_REG, '') + '</h1>';
    });
    
    var match = body.match(SCORE_REG);
    if (match)
      valid.push({
        user: getAuthorName(a),
        size: +match[2],
        language: match[1],
        link: a.share_link,
      });
    
  });
  
  valid.sort(function (a, b) {
    var aB = a.size,
        bB = b.size;
    return aB - bB
  });

  var languages = {};
  var place = 1;
  var lastSize = null;
  var lastPlace = 1;
  valid.forEach(function (a) {
    if (a.size != lastSize)
      lastPlace = place;
    lastSize = a.size;
    ++place;
    
    var answer = jQuery("#answer-template").html();
    answer = answer.replace("{{PLACE}}", lastPlace + ".")
                   .replace("{{NAME}}", a.user)
                   .replace("{{LANGUAGE}}", a.language)
                   .replace("{{SIZE}}", a.size)
                   .replace("{{LINK}}", a.link);
    answer = jQuery(answer);
    jQuery("#answers").append(answer);

    var lang = a.language;
    if (! /<a/.test(lang)) lang = '<i>' + lang + '</i>';
    lang = jQuery(lang).text().toLowerCase();

    languages[lang] = languages[lang] || {lang: a.language, user: a.user, size: a.size, link: a.link, uniq: lang};
  });

  var langs = [];
  for (var lang in languages)
    if (languages.hasOwnProperty(lang))
      langs.push(languages[lang]);

  langs.sort(function (a, b) {
    if (a.uniq > b.uniq) return 1;
    if (a.uniq < b.uniq) return -1;
    return 0;
  });

  for (var i = 0; i < langs.length; ++i)
  {
    var language = jQuery("#language-template").html();
    var lang = langs[i];
    language = language.replace("{{LANGUAGE}}", lang.lang)
                       .replace("{{NAME}}", lang.user)
                       .replace("{{SIZE}}", lang.size)
                       .replace("{{LINK}}", lang.link);
    language = jQuery(language);
    jQuery("#languages").append(language);
  }

}
body { text-align: left !important}

#answer-list {
  padding: 10px;
  width: 290px;
  float: left;
}

#language-list {
  padding: 10px;
  width: 290px;
  float: left;
}

table thead {
  font-weight: bold;
}

table td {
  padding: 5px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/Sites/codegolf/primary.css?v=f52df912b654">
<div id="answer-list">
  <h2>Leaderboard</h2>
  <table class="answer-list">
    <thead>
      <tr><td></td><td>Author</td><td>Language</td><td>Size</td></tr>
    </thead>
    <tbody id="answers">

    </tbody>
  </table>
</div>
<div id="language-list">
  <h2>Winners by Language</h2>
  <table class="language-list">
    <thead>
      <tr><td>Language</td><td>User</td><td>Score</td></tr>
    </thead>
    <tbody id="languages">

    </tbody>
  </table>
</div>
<table style="display: none">
  <tbody id="answer-template">
    <tr><td>{{PLACE}}</td><td>{{NAME}}</td><td>{{LANGUAGE}}</td><td><a href="{{LINK}}">{{SIZE}}</a></td></tr>
  </tbody>
</table>
<table style="display: none">
  <tbody id="language-template">
    <tr><td>{{LANGUAGE}}</td><td>{{NAME}}</td><td><a href="{{LINK}}">{{SIZE}}</a></td></tr>
  </tbody>
</table>

\$\endgroup\$
  • \$\begingroup\$ (1) the score is only negative if there is any bonus. I don't think that's very relevant. (2) what's the difference between the css? \$\endgroup\$ – user202729 May 28 '18 at 13:54
  • \$\begingroup\$ @user202729, now is different again. SE devs changed the stylesheet URL in meantime. That is why would be nice to have a Permaproxy to the current stylesheet. \$\endgroup\$ – manatwork May 28 '18 at 14:32
  • \$\begingroup\$ Should be done now. \$\endgroup\$ – user202729 May 31 '18 at 5:22
3
\$\begingroup\$

Soft-linkable Leaderboard

Adding this as an answer with a single snippet in order to simply reuse the leaderboard snippet using Calvin'sHobbies' Arbitrary stack snippet loader

To use this, simply make sure that you have the snippet from the above link in your post with the configurations as below:

  • site = 'meta.codegolf'
  • postID = 5314
  • isAnswer = true

and an additional configuration required by the leaderboard snippet:

  • QUESTION_ID = id of the question for which leaderboard is required. Ex. 47338

/* Configuration */

// It will be like http://XYZ.stackexchange.com/questions/QUESTION_ID/... on any question page
var ANSWER_FILTER = "!t)IWYnsLAZle2tQ3KqrVveCRJfxcRLe";

/* App */

var answers = [], page = 1;

function answersUrl(index) {
  return "http://api.stackexchange.com/2.2/questions/" +  QUESTION_ID + "/answers?page=" + index + "&pagesize=100&order=desc&sort=creation&site=codegolf&filter=" + ANSWER_FILTER;
}

function getAnswers() {
  jQuery.ajax({
    url: answersUrl(page++),
    method: "get",
    dataType: "jsonp",
    crossDomain: true,
    success: function (data) {
      answers.push.apply(answers, data.items);
      if (data.has_more) getAnswers();
      else process();
    }
  });
}

getAnswers();

var SIZE_REG = /\d+(?=[^\d&]*(?:&lt;(?:s&gt;[^&]*&lt;\/s&gt;|[^&]+&gt;)[^\d&]*)*$)/;
var NUMBER_REG = /\d+/;
var LANGUAGE_REG = /^#*\s*([^,]+)/;

function shouldHaveHeading(a) {
  var pass = false;
  var lines = a.body_markdown.split("\n");
  try {
    pass |= /^#/.test(a.body_markdown);
    pass |= ["-", "="]
              .indexOf(lines[1][0]) > -1;
    pass &= LANGUAGE_REG.test(a.body_markdown);
  } catch (ex) {}
  return pass;
}

function shouldHaveScore(a) {
  var pass = false;
  try {
    pass |= SIZE_REG.test(a.body_markdown.split("\n")[0]);
  } catch (ex) {}
  return pass;
}

function getAuthorName(a) {
  return a.owner.display_name;
}

function process() {
  answers = answers.filter(shouldHaveScore)
                   .filter(shouldHaveHeading);
  answers.sort(function (a, b) {
    var aB = +(a.body_markdown.split("\n")[0].match(SIZE_REG) || [Infinity])[0],
        bB = +(b.body_markdown.split("\n")[0].match(SIZE_REG) || [Infinity])[0];
    return aB - bB
  });

  var languages = {};
  var place = 1;
  var lastSize = null;
  var lastPlace = 1;
  answers.forEach(function (a) {
    var headline = a.body_markdown.split("\n")[0];
    //console.log(a);
    var answer = jQuery("#answer-template").html();
    var num = headline.match(NUMBER_REG)[0];
    var size = (headline.match(SIZE_REG)||[0])[0];
    var language = headline.match(LANGUAGE_REG)[1];
    var user = getAuthorName(a);
    if (size != lastSize)
      lastPlace = place;
    lastSize = size;
    ++place;
    answer = answer.replace("{{PLACE}}", lastPlace + ".")
                   .replace("{{NAME}}", user)
                   .replace("{{LANGUAGE}}", language)
                   .replace("{{SIZE}}", size)
                   .replace("{{LINK}}", a.share_link);
    answer = jQuery(answer)
    jQuery("#answers").append(answer);

    languages[language] = languages[language] || {lang: language, user: user, size: size, link: a.share_link};
  });

  var langs = [];
  for (var lang in languages)
    if (languages.hasOwnProperty(lang))
      langs.push(languages[lang]);

  langs.sort(function (a, b) {
    if (a.lang > b.lang) return 1;
    if (a.lang < b.lang) return -1;
    return 0;
  });

  for (var i = 0; i < langs.length; ++i)
  {
    var language = jQuery("#language-template").html();
    var lang = langs[i];
    language = language.replace("{{LANGUAGE}}", lang.lang)
                       .replace("{{NAME}}", lang.user)
                       .replace("{{SIZE}}", lang.size)
                       .replace("{{LINK}}", lang.link);
    language = jQuery(language);
    jQuery("#languages").append(language);
  }

}
body { text-align: left !important}

#answer-list {
  padding: 10px;
  width: 290px;
  float: left;
}

#language-list {
  padding: 10px;
  width: 290px;
  float: left;
}

table thead {
  font-weight: bold;
}

table td {
  padding: 5px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/codegolf/all.css?v=83c949450c8b">
<div id="answer-list">
  <h2>Leaderboard</h2>
  <table class="answer-list">
    <thead>
      <tr><td></td><td>Author</td><td>Language</td><td>Size</td></tr>
    </thead>
    <tbody id="answers">

    </tbody>
  </table>
</div>
<div id="language-list">
  <h2>Winners by Language</h2>
  <table class="language-list">
    <thead>
      <tr><td>Language</td><td>User</td><td>Score</td></tr>
    </thead>
    <tbody id="languages">

    </tbody>
  </table>
</div>
<table style="display: none">
  <tbody id="answer-template">
    <tr><td>{{PLACE}}</td><td>{{NAME}}</td><td>{{LANGUAGE}}</td><td>{{SIZE}}</td><td><a href="{{LINK}}">Link</a></td></tr>
  </tbody>
</table>
<table style="display: none">
  <tbody id="language-template">
    <tr><td>{{LANGUAGE}}</td><td>{{NAME}}</td><td>{{SIZE}}</td><td><a href="{{LINK}}">Link</a></td></tr>
  </tbody>
</table>

\$\endgroup\$
  • \$\begingroup\$ I'm no longer sure this is a good idea. Now I can never make a breaking change to the leaderboard snippet without affecting an unknown number of older challenges. \$\endgroup\$ – Martin Ender Aug 19 '15 at 8:32
  • \$\begingroup\$ @MartinBüttner Your breaking change should go in a new snippet. Most probably its a new feature addition, or a new syntax.. That anyways is not needed by the current users of the script. \$\endgroup\$ – Optimizer Aug 19 '15 at 9:22
  • \$\begingroup\$ I don't intend to post a new question or answer every time I add a feature... that just clutters up meta. \$\endgroup\$ – Martin Ender Aug 19 '15 at 9:23
  • \$\begingroup\$ Well, you cannot have the cake and eat it too. You want re usability, but want to make breaking change too. Your solution works with answers too, so why not put a new answer to an existing question \$\endgroup\$ – Optimizer Aug 19 '15 at 9:26
  • \$\begingroup\$ Is there any way to make Helka's snippet point to a previous version in the revision history rather than needing separate answers for new versions? \$\endgroup\$ – trichoplax Apr 5 '16 at 15:37
3
\$\begingroup\$

Formatting - Set a default font

Let's be honest. Most browsers' default font (usually Times New Roman) is pretty ugly. Let's fix that. And propose fonts please!

\$\endgroup\$
  • \$\begingroup\$ Roboto? Sans Serif? \$\endgroup\$ – Matthew Roh Feb 12 '17 at 7:10
  • \$\begingroup\$ @SIGSEGV Roboto is quite nice \$\endgroup\$ – Kritixi Lithos Mar 21 '17 at 16:31
  • \$\begingroup\$ Also possible (and elegant): Noto \$\endgroup\$ – Matthew Roh Mar 21 '17 at 16:35
  • \$\begingroup\$ I find Times New Roman pretty readable... \$\endgroup\$ – user202729 May 28 '18 at 14:20
2
\$\begingroup\$

Makes links target="_top" or target="_blank"

That way they don't open in the snippet. I find that annoying.

\$\endgroup\$
  • \$\begingroup\$ Doesn't seem to work for me. Tested. \$\endgroup\$ – user202729 Dec 27 '17 at 13:49
2
\$\begingroup\$

Permaproxy to the current stylesheet

(This feature request is not for the Leaderboare Snippet maintainer, but for the PPCG proxy owners.)

The Leaderboard Snippet includes the same stylesheet as the PPCG site itself uses. That stylesheet's URL can not be acquired programmatically by the snippet, so it is hardcoded:

<link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/codegolf/all.css?v=83c949450c8b">

And this is how the snippet was included in 150++ questions.

Then PPCG graduated and the site's stylesheet changed:

<link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/Sites/codegolf/primary.css?v=f52df912b654">

Now all those questions posted before graduation (and even those after) left without stylesheet and their layout suffered, ranging between ugly and unreadable/unclickable.

As further style changes will probably happen (see Graduation Design Ideas's popularity), would be nice if one of the PPCG proxy owners would set up a permanent redirect towards the current stylesheet used by the site:

Redirect /official.css //cdn.sstatic.net/Sites/codegolf/primary.css?v=f52df912b654

So Leaderboard Snippet could then include that stylesheet with permanent URL:

<link rel="stylesheet" type="text/css" href="//pp.cg/official.css">
\$\endgroup\$
1
\$\begingroup\$

Tip: Leader board for cops n' robbers

  1. To sort ascending: search for r-a - there should be one instance, replace it with a-r.
  2. To ignore cracked entries: search for SCORE_REG - there should be two instances, the second one is (at the time of this answer) the regex:
    /<h\d>\s*([^\n,]*[^\s,]),.*?(\d+)(?=[^\n\d<>]*(?:<(?:s>[^\n<>]*<\/s>|[^\n<>]+>)*[^\n\d<>]*)*<\/h\d>)/
    replace it with:
    /<h\d>\s*([^\n,]*[^\s,]),.*?(\d+)(?=[^\n\d<>]*(?:<(?:s>[^\n<>]*<\/s>|[^\n<>]+>)[^\n\d<>]*)^(?:(?!cracked)\i.)*<\/h\d>)/
    (that is add ^(?:(?!cracked)\i.) to not include those headers containing "cracked" with any case (\i).

Please edit to fix bugs, regex is not my forté.
- I did notice it broke with the "bytes" text of the header being a link, so maybe ^(?:(?!cracked)\i.) should actually be placed earlier?

\$\endgroup\$
1
\$\begingroup\$

This answer can be used for experimenting with Martin's snippet. You may edit this to your own desire

<script>var QUESTION_ID=99026,OVERRIDE_USER=0;</script><style>body{text-align:left!important}#answer-list{padding:10px;float:left}#language-list{padding:10px;float:left}table thead{font-weight:700}table td{padding:5px}</style><script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><script>var ANSWER_FILTER="!t)IWYnsLAZle2tQ3KqrVveCRJfxcRLe",COMMENT_FILTER="!)Q2B_A2kjfAiU78X(md6BoYk",answers=[],answers_hash,answer_ids,answer_page=1,more_answers=!0,comment_page;function answersUrl(d){return"https://api.stackexchange.com/2.2/questions/"+QUESTION_ID+"/answers?page="+d+"&pagesize=100&order=desc&sort=creation&site=codegolf&filter="+ANSWER_FILTER}function commentUrl(d,e){return"https://api.stackexchange.com/2.2/answers/"+e.join(";")+"/comments?page="+d+"&pagesize=100&order=desc&sort=creation&site=codegolf&filter="+COMMENT_FILTER}function getAnswers(){jQuery.ajax({url:answersUrl(answer_page++),method:"get",dataType:"jsonp",crossDomain:!0,success:function(d){answers.push.apply(answers,d.items),answers_hash=[],answer_ids=[],d.items.forEach(function(e){e.comments=[];var f=+e.share_link.match(/\d+/);answer_ids.push(f),answers_hash[f]=e}),d.has_more||(more_answers=!1),comment_page=1,getComments()}})}function getComments(){jQuery.ajax({url:commentUrl(comment_page++,answer_ids),method:"get",dataType:"jsonp",crossDomain:!0,success:function(d){d.items.forEach(function(e){e.owner.user_id===OVERRIDE_USER&&answers_hash[e.post_id].comments.push(e)}),d.has_more?getComments():more_answers?getAnswers():process()}})}getAnswers();var SCORE_REG=function(){var d=String.raw`h\d`,e=String.raw`\-?\d+\.?\d*`,f=String.raw`[^\n<>]*`,g=String.raw`<s>${f}</s>|<strike>${f}</strike>|<del>${f}</del>`,h=String.raw`[^\n\d<>]*`,j=String.raw`<[^\n<>]+>`;return new RegExp(String.raw`<${d}>`+String.raw`\s*([^\n,]*[^\s,]),.*?`+String.raw`(${e})`+String.raw`(?=`+String.raw`${h}`+String.raw`(?:(?:${g}|${j})${h})*`+String.raw`</${d}>`+String.raw`)`)}(),OVERRIDE_REG=/^Override\s*header:\s*/i;function getAuthorName(d){return d.owner.display_name}function process(){var d=[];answers.forEach(function(n){var o=n.body;n.comments.forEach(function(q){OVERRIDE_REG.test(q.body)&&(o="<h1>"+q.body.replace(OVERRIDE_REG,"")+"</h1>")});var p=o.match(SCORE_REG);p&&d.push({user:getAuthorName(n),size:+p[2],language:p[1],link:n.share_link})}),d.sort(function(n,o){var p=n.size,q=o.size;return p-q});var e={},f=1,g=null,h=1;d.forEach(function(n){n.size!=g&&(h=f),g=n.size,++f;var o=jQuery("#answer-template").html();o=o.replace("{{PLACE}}",h+".").replace("{{NAME}}",n.user).replace("{{LANGUAGE}}",n.language).replace("{{SIZE}}",n.size).replace("{{LINK}}",n.link),o=jQuery(o),jQuery("#answers").append(o);var p=n.language;p=jQuery("<i>"+n.language+"</i>").text().toLowerCase(),e[p]=e[p]||{lang:n.language,user:n.user,size:n.size,link:n.link,uniq:p}});var j=[];for(var k in e)e.hasOwnProperty(k)&&j.push(e[k]);j.sort(function(n,o){return n.uniq>o.uniq?1:n.uniq<o.uniq?-1:0});for(var l=0;l<j.length;++l){var m=jQuery("#language-template").html(),k=j[l];m=m.replace("{{LANGUAGE}}",k.lang).replace("{{NAME}}",k.user).replace("{{SIZE}}",k.size).replace("{{LINK}}",k.link),m=jQuery(m),jQuery("#languages").append(m)}}</script><link rel="stylesheet" type="text/css" href="https://cdn.sstatic.net/Sites/codegolf/primary.css?v=f52df912b654"> <div id="language-list"> <h2>Winners by Language</h2> <table class="language-list"> <thead> <tr><td>Language</td><td>User</td><td>Score</td></tr></thead> <tbody id="languages"> </tbody> </table> </div><div id="answer-list"> <h2>Leaderboard</h2> <table class="answer-list"> <thead> <tr><td></td><td>Author</td><td>Language</td><td>Size</td></tr></thead> <tbody id="answers"> </tbody> </table> </div><table style="display: none"> <tbody id="answer-template"> <tr><td>{{PLACE}}</td><td>{{NAME}}</td><td>{{LANGUAGE}}</td><td><a href="{{LINK}}">{{SIZE}}</a></td></tr></tbody> </table> <table style="display: none"> <tbody id="language-template"> <tr><td>{{LANGUAGE}}</td><td>{{NAME}}</td><td><a href="{{LINK}}">{{SIZE}}</a></td></tr></tbody> </table> 

\$\endgroup\$
0
\$\begingroup\$

Tie break on first posted

Ideally, if there is a joint first place I'd like to see the first posted appear at the top. This is complicated by the fact that the time of the post is not necessarily the time that the code first had that length.

I'm not suggesting the snippet do an analysis of the edit histories, but I'm posting this in case there's a simpler way. Maybe a comment override?

\$\endgroup\$
  • 3
    \$\begingroup\$ Automating this definitely seems tricky, because not only would you have to figure out in the edit history when the score was first reached, but it might also happen that the score was reached before, but the answer was invalid at the time and only a later edit actually achieved the score with a valid solution. (Also, grace period...) \$\endgroup\$ – Martin Ender May 25 '16 at 10:45
  • \$\begingroup\$ It's even worse than I thought then... So I guess the only way would be with a comment override of some kind, but unless this answer gets lots of upvotes I'm guessing that's unnecessary effort. \$\endgroup\$ – trichoplax May 25 '16 at 10:51
  • \$\begingroup\$ Good point about the grace period - I guess that means even a human observer can't tell for certain which was first if the edit times are within 5 minutes of each other. \$\endgroup\$ – trichoplax May 25 '16 at 10:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .