<commit_before>var testerPresets = {
  'blank': {},
  'email': {
    // Note that backslashes have to be escaped by backslashes…
    'pattern': '/\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b/ig',
    // Note that it appears we can't have literal line breaks in JavaScript
    // strings; hence the \n mess below.
    'testText': "This pattern matches email addresses. It was culled from Jan Goyvaerts\' wonderful Regular-Expressions.info site, as found on  http://www.regular-expressions.info/email.html , so all credit to him. Though it is not the official pattern to match email addresses, it is far simpler while still broad enough to work nearly all the time. Here\'s some test email addresses to match or not match:\n\nalbright@abweb.us\nbillg@microsoft.com\nfoo@example..org\ntcook@apple.com\ntesting@subdomain.example.net\nfailure@example\n\nHere\'s an example of an email address which will fail with this pattern, even though it\'s technically correct:\ninfo@about.museum"
  },
  'non-ascii': {
    'pattern': '/[^\\x00-\\x7f]+/g',
    'testText': "こんにちは！ This pattern will find contiguous lengths of non-ASCII characters, including heavy metal ümlauts. Note that it won\'t catch tricky non-printable but legal ASCII characters such as the bell or escape characters, though. C\'est la vie. ¿Podemos ir a las montañas?"
  },
  'na-tel': {
    'pattern': '/(\\(\\d{3}\\)|\\b\\d{3})[- ]?\\d{3}[- ]?\\d{4}\\b/g',
    'testText': "This pattern is designed to extract North American telephone numbers (with area codes) in various common formats.\n505-555-4567, (707) 555-1234, 759 555 7890, 8185554860\nNot-so-common but still not quite nonsense:\n(505)555-4567, (423)5559999, 123-5557891\n\nHere's some bad numbers which shouldn't match.\n(555) 555 123, 555 1235, 5555-1236, (123) 555-12345, (1234) 555-9984"
  },
  'repeating': {
    'pattern': '/(\\w)(\\1+)/g',
    'testText': "Welcome to 2problems.com, the friendly JavaScript-powered regular expression tester! Now you have two problems…\n\nRegular expressions are a powerful tool for programmers for finding bits of text inside of other text. For example, the pattern in the field above finds sequences of two or more repeated letters that appear in this text. (If you aren't a programmer, you probably won't find regular expressions, or this site, very useful.) Isn't it cooooooooooool? :)\n\nTo test an expression of your own, change the pattern in the field above, or use the \"New tester\" menu below to add a new tester to the page. For more info about 2problems.com, click the \"About\" link in the sidebar to the right."
  }
}

$(document).ready(function() {

  // Tester model.
  var TesterModel = Backbone.Model.extend({
    'defaults': function() {
      var order_id = testers.nextOrderVal();
      return {
        'id': order_id,
        'order': order_id,
        'pattern': '',
        'testText': '',
        'hash': '',
      };
    },
    'matches': null,
    // Store the compiled pattern.
    'compiledPattern': null,
    // Store matches.
    // Parse the pattern string and compile the pattern. Returns true or false
    // based on success of the new pattern compilation.
    'compilePattern': function(pattern) {
      // The first character must be the pattern delimiter.
      var patternDelimiter = pattern.charAt(0),
        // Build a pattern to extract the pattern and its flags.
        patternPattern = new RegExp('\\' + patternDelimiter + '(.+)\\' + patternDelimiter + '(.*)'),
        patternParts = pattern.match(patternPattern);
      // If we're building a new pattern, we want to flush the old match
      // results.
      this.matches = null;
      try {
        this.compiledPattern = new RegExp(patternParts[1], patternParts[2]);
      }
      catch (e) {
        this.compiledPattern = null;
        return false;
      }
      return true;
    },
    // Run our compiled pattern against text and store the results.
    'executePattern': function(haystack) {
      this.matches = haystack.match(this.compiledPattern);
    }
  });

  // Collection of testers.
  var TesterCollection = Backbone.Collection.extend({
    'model': TesterModel,
    'nextOrderVal': function () {
      return this.length ? this.last().id + 1 : 0;
    },
    'localStorage': new Backbone.LocalStorage('2problems-dot-com')
  })

  // Instantiate a collection.
  var testers = new TesterCollection;
  testers.fetch();

  // View for testers. Uses a "template" in the HTML.
  var TesterView = Backbone.View.extend({
    'tagName': 'article',
    'template': _.template($('#tester-t').html()),
    'events': {
      'keyup .pattern': 'sendPattern',
      'keyup .haystack': 'sendHaystack',
      'click .copy': 'cloneSelf',
      'click .close': 'closeSelf'
    },
    'render': function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$el.attr({'id': 'tester-' + this.model.id, 'class': 'tester'});
      if (this.model.get('pattern') !== undefined) {
        this.sendPattern();
      }
      if (this.model.get('isHighlighted')) {
        $('form', this.$el).addClass('highlighted');
      }
      this.$el.slideDown();
      return this;
    },
    'renderMatches': function() {
      var results = this.model.matches,
        $resultsRegion = $('.results', this.$el);
      // Remove things in the results region that aren't the header.
      $(':not(h2)', $resultsRegion).remove();
      // Don't put anything in the region if the pattern field is just an empty
      // string.
      if (this.model.get('pattern')) {
        // Note that String.match() returns null instead of just an empty array
        // in the case of no matches.
        if (this.model.compiledPattern === null) {
          var $results = $('<div>').addClass('error').text('(invalid pattern)');
        }
        else if (results === null) {
          var $results = $('<div>').text('(no matches)');
        }
        else {
          var $results = $('<ol>');
          _.each(results, function(result) {
            $results.append($('<li>').text(result));
          });
        }
        $resultsRegion.append($results);
      }
    },
    'sendPattern': function() {
      // @todo We probably want to re-engineer this so that sending the pattern
      // to the model and trying to complile it are two seperate operations, as
      // we actually want to do the latter without doing the former when
      // instantiatng from localStorage.
      var patternText = $('.pattern', this.$el).val(),
        haystackText = $('.haystack', this.$el).val(),
        hashUrl = '';
      this.model.set('pattern', patternText);
      if (!patternText || !this.model.compilePattern(patternText)) {
        // We don't have a pattern to compile, or compilation failed. Skip
        // sending the haystack and jump directly to  renderMatches(), which
        // will show an error if necessary.
        this.model.save({'pattern': patternText});
        this.renderMatches();
        $('a.hash', this.$el).hide();
      }
      else {
        // Send the haystack. Subsequently, the matches will be show in the
        // results area for this tester.
        this.sendHaystack();
      }
    },
    'sendHaystack': function() {
      var haystack = $('.haystack', this.$el).val();
      this.model.set('testText', haystack);
      this.model.executePattern(haystack);
      this.renderMatches();
      this.model.save({'testText': haystack});
      this.setHash();
    },
    'cloneSelf': function() {
      var tester = new TesterModel({'pattern': this.model.get('pattern'), 'testText': this.model.get('testText')});
      testers.add(tester, {'at': this.model.id});
    },
    'closeSelf': function() {
      this.model.destroy();
      this.$el.slideUp();
    },
    'setHash': function() {
      var patternText = this.model.get('pattern'),
        haystackText = this.model.get('testText'),
        hashUrl = document.location.protocol + '//' + document.location.hostname + document.location.pathname + '#' + escape(patternText + patternText.charAt(0) + haystackText);
      if (hashUrl.length < 2083) {
        $('a.hash', this.$el).show().attr({'href': hashUrl});
      }
      else {
        $('a.hash', this.$el).hide();
      }
    }
  });

  // The "app" view.
  var TwoPView = Backbone.View.extend({
    'el': $('#main'),
    'events': {
      'click footer input#add': 'addTester'
    },
    'initialize': function() {
      var delimiter, matches;
      // See if we can create a tester from the hash
      // A hash will look like:
      // #/foo/i/foobarappasdf
      // Compiled pattern will look like:
      // /^#(\/[^\/]+/(?:[gi]+)?)\/(.+)/
      if (document.location.hash.length > 1) {
        delimiter = document.location.hash.charAt(1);
        matches = document.location.hash.match(RegExp('^#(\\' + delimiter + '[^\\' + delimiter + ']+\\' + delimiter + '(?:[gi]+)?)\\' + delimiter + '(.+)'));
        if (matches) {
          var testerFromHash = new TesterModel({'pattern': matches[1], 'testText': matches[2]});
          testerFromHash.set('isHighlighted', true);
          testers.push(testerFromHash);
        }
      }
      if (testers.length === 0) {
        // No testers were loaded, so create a 'repeating' tester and add it to
        // the page since its test text has a nice welcome message.
        var initialTester = new TesterModel(testerPresets.repeating);
        testers.push(initialTester);
      }
      this.listenTo(testers, 'add', this.addOne);

      $('.toggle-about').click(function() {
        if ($('body').hasClass('header-open')) {
          $('body').removeClass('header-open');
          $('header #open-about').text('About');
        }
        else {
          $('body').addClass('header-open');
          $('header #open-about').text('Close about text');
        }
      });
    },
    'render': function() {
      testers.each(function(tester) {
        var view = new TesterView({'model': tester});
        // "This" is the window…?
        this.$('#main footer').before(view.render().el);
      });
      // Remove the "No JS" warning.
      this.$('#third-problem').remove();
      // Show the footer.
      this.$('footer').show();
    },
    'addOne': function(tester) {
      var view = new TesterView({'model': tester});
      this.$('footer').before(view.render().el);
    },
    'addTester': function() {
      var testerPresetName = this.$('footer select#new-type').val(),
        newTester = new TesterModel(testerPresets[testerPresetName]);
      testers.push(newTester);
    }
  });

  var TwoProblems = new TwoPView;
  TwoProblems.render();

});
<commit_msg>The About "page" can be directly linked to with hash magic.<commit_after>var testerPresets = {
  'blank': {},
  'email': {
    // Note that backslashes have to be escaped by backslashes…
    'pattern': '/\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b/ig',
    // Note that it appears we can't have literal line breaks in JavaScript
    // strings; hence the \n mess below.
    'testText': "This pattern matches email addresses. It was culled from Jan Goyvaerts\' wonderful Regular-Expressions.info site, as found on  http://www.regular-expressions.info/email.html , so all credit to him. Though it is not the official pattern to match email addresses, it is far simpler while still broad enough to work nearly all the time. Here\'s some test email addresses to match or not match:\n\nalbright@abweb.us\nbillg@microsoft.com\nfoo@example..org\ntcook@apple.com\ntesting@subdomain.example.net\nfailure@example\n\nHere\'s an example of an email address which will fail with this pattern, even though it\'s technically correct:\ninfo@about.museum"
  },
  'non-ascii': {
    'pattern': '/[^\\x00-\\x7f]+/g',
    'testText': "こんにちは！ This pattern will find contiguous lengths of non-ASCII characters, including heavy metal ümlauts. Note that it won\'t catch tricky non-printable but legal ASCII characters such as the bell or escape characters, though. C\'est la vie. ¿Podemos ir a las montañas?"
  },
  'na-tel': {
    'pattern': '/(\\(\\d{3}\\)|\\b\\d{3})[- ]?\\d{3}[- ]?\\d{4}\\b/g',
    'testText': "This pattern is designed to extract North American telephone numbers (with area codes) in various common formats.\n505-555-4567, (707) 555-1234, 759 555 7890, 8185554860\nNot-so-common but still not quite nonsense:\n(505)555-4567, (423)5559999, 123-5557891\n\nHere's some bad numbers which shouldn't match.\n(555) 555 123, 555 1235, 5555-1236, (123) 555-12345, (1234) 555-9984"
  },
  'repeating': {
    'pattern': '/(\\w)(\\1+)/g',
    'testText': "Welcome to 2problems.com, the friendly JavaScript-powered regular expression tester! Now you have two problems…\n\nRegular expressions are a powerful tool for programmers for finding bits of text inside of other text. For example, the pattern in the field above finds sequences of two or more repeated letters that appear in this text. (If you aren't a programmer, you probably won't find regular expressions, or this site, very useful.) Isn't it cooooooooooool? :)\n\nTo test an expression of your own, change the pattern in the field above, or use the \"New tester\" menu below to add a new tester to the page. For more info about 2problems.com, click the \"About\" link in the sidebar to the right."
  }
}

$(document).ready(function() {

  // Tester model.
  var TesterModel = Backbone.Model.extend({
    'defaults': function() {
      var order_id = testers.nextOrderVal();
      return {
        'id': order_id,
        'order': order_id,
        'pattern': '',
        'testText': '',
        'hash': '',
      };
    },
    'matches': null,
    // Store the compiled pattern.
    'compiledPattern': null,
    // Store matches.
    // Parse the pattern string and compile the pattern. Returns true or false
    // based on success of the new pattern compilation.
    'compilePattern': function(pattern) {
      // The first character must be the pattern delimiter.
      var patternDelimiter = pattern.charAt(0),
        // Build a pattern to extract the pattern and its flags.
        patternPattern = new RegExp('\\' + patternDelimiter + '(.+)\\' + patternDelimiter + '(.*)'),
        patternParts = pattern.match(patternPattern);
      // If we're building a new pattern, we want to flush the old match
      // results.
      this.matches = null;
      try {
        this.compiledPattern = new RegExp(patternParts[1], patternParts[2]);
      }
      catch (e) {
        this.compiledPattern = null;
        return false;
      }
      return true;
    },
    // Run our compiled pattern against text and store the results.
    'executePattern': function(haystack) {
      this.matches = haystack.match(this.compiledPattern);
    }
  });

  // Collection of testers.
  var TesterCollection = Backbone.Collection.extend({
    'model': TesterModel,
    'nextOrderVal': function () {
      return this.length ? this.last().id + 1 : 0;
    },
    'localStorage': new Backbone.LocalStorage('2problems-dot-com')
  })

  // Instantiate a collection.
  var testers = new TesterCollection;
  testers.fetch();

  // View for testers. Uses a "template" in the HTML.
  var TesterView = Backbone.View.extend({
    'tagName': 'article',
    'template': _.template($('#tester-t').html()),
    'events': {
      'keyup .pattern': 'sendPattern',
      'keyup .haystack': 'sendHaystack',
      'click .copy': 'cloneSelf',
      'click .close': 'closeSelf'
    },
    'render': function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$el.attr({'id': 'tester-' + this.model.id, 'class': 'tester'});
      if (this.model.get('pattern') !== undefined) {
        this.sendPattern();
      }
      if (this.model.get('isHighlighted')) {
        $('form', this.$el).addClass('highlighted');
      }
      this.$el.slideDown();
      return this;
    },
    'renderMatches': function() {
      var results = this.model.matches,
        $resultsRegion = $('.results', this.$el);
      // Remove things in the results region that aren't the header.
      $(':not(h2)', $resultsRegion).remove();
      // Don't put anything in the region if the pattern field is just an empty
      // string.
      if (this.model.get('pattern')) {
        // Note that String.match() returns null instead of just an empty array
        // in the case of no matches.
        if (this.model.compiledPattern === null) {
          var $results = $('<div>').addClass('error').text('(invalid pattern)');
        }
        else if (results === null) {
          var $results = $('<div>').text('(no matches)');
        }
        else {
          var $results = $('<ol>');
          _.each(results, function(result) {
            $results.append($('<li>').text(result));
          });
        }
        $resultsRegion.append($results);
      }
    },
    'sendPattern': function() {
      // @todo We probably want to re-engineer this so that sending the pattern
      // to the model and trying to complile it are two seperate operations, as
      // we actually want to do the latter without doing the former when
      // instantiatng from localStorage.
      var patternText = $('.pattern', this.$el).val(),
        haystackText = $('.haystack', this.$el).val(),
        hashUrl = '';
      this.model.set('pattern', patternText);
      if (!patternText || !this.model.compilePattern(patternText)) {
        // We don't have a pattern to compile, or compilation failed. Skip
        // sending the haystack and jump directly to  renderMatches(), which
        // will show an error if necessary.
        this.model.save({'pattern': patternText});
        this.renderMatches();
        $('a.hash', this.$el).hide();
      }
      else {
        // Send the haystack. Subsequently, the matches will be show in the
        // results area for this tester.
        this.sendHaystack();
      }
    },
    'sendHaystack': function() {
      var haystack = $('.haystack', this.$el).val();
      this.model.set('testText', haystack);
      this.model.executePattern(haystack);
      this.renderMatches();
      this.model.save({'testText': haystack});
      this.setHash();
    },
    'cloneSelf': function() {
      var tester = new TesterModel({'pattern': this.model.get('pattern'), 'testText': this.model.get('testText')});
      testers.add(tester, {'at': this.model.id});
    },
    'closeSelf': function() {
      this.model.destroy();
      this.$el.slideUp();
    },
    'setHash': function() {
      var patternText = this.model.get('pattern'),
        haystackText = this.model.get('testText'),
        hashUrl = document.location.protocol + '//' + document.location.hostname + document.location.pathname + '#' + escape(patternText + patternText.charAt(0) + haystackText);
      if (hashUrl.length < 2083) {
        $('a.hash', this.$el).show().attr({'href': hashUrl});
      }
      else {
        $('a.hash', this.$el).hide();
      }
    }
  });

  // The "app" view.
  var TwoPView = Backbone.View.extend({
    'el': $('#main'),
    'events': {
      'click footer input#add': 'addTester'
    },
    'initialize': function() {
      var delimiter, matches;

      $('.toggle-about').click(function() {
        if ($('body').hasClass('header-open')) {
          $('body').removeClass('header-open');
          $('header #open-about').text('About');
          document.location.hash = '';
        }
        else {
          $('body').addClass('header-open');
          $('header #open-about').text('Close about text');
        }
      });

      if (document.location.hash === '#about') {
        $('.toggle-about').eq(0).click();
      }
      // See if we can create a tester from the hash
      // A hash will look like:
      // #/foo/i/foobarappasdf
      // Compiled pattern will look like:
      // /^#(\/[^\/]+/(?:[gi]+)?)\/(.+)/
      else if (document.location.hash.length > 1) {
        delimiter = document.location.hash.charAt(1);
        matches = document.location.hash.match(RegExp('^#(\\' + delimiter + '[^\\' + delimiter + ']+\\' + delimiter + '(?:[gi]+)?)\\' + delimiter + '(.+)'));
        if (matches) {
          var testerFromHash = new TesterModel({'pattern': matches[1], 'testText': matches[2]});
          testerFromHash.set('isHighlighted', true);
          testers.push(testerFromHash);
        }
      }
      if (testers.length === 0) {
        // No testers were loaded, so create a 'repeating' tester and add it to
        // the page since its test text has a nice welcome message.
        var initialTester = new TesterModel(testerPresets.repeating);
        testers.push(initialTester);
      }
      this.listenTo(testers, 'add', this.addOne);

    },
    'render': function() {
      testers.each(function(tester) {
        var view = new TesterView({'model': tester});
        // "This" is the window…?
        this.$('#main footer').before(view.render().el);
      });
      // Remove the "No JS" warning.
      this.$('#third-problem').remove();
      // Show the footer.
      this.$('footer').show();
    },
    'addOne': function(tester) {
      var view = new TesterView({'model': tester});
      this.$('footer').before(view.render().el);
    },
    'addTester': function() {
      var testerPresetName = this.$('footer select#new-type').val(),
        newTester = new TesterModel(testerPresets[testerPresetName]);
      testers.push(newTester);
    }
  });

  var TwoProblems = new TwoPView;
  TwoProblems.render();

});
<|endoftext|>


<commit_before>/**
 * Concurrently run our various dev tasks.
 *
 * Usage: node dev
 **/

const app = require('.')
    , chalk = require('chalk'), {bold} = chalk
    , {red, green, blue, cyan, yellow} = bold
    , dev = module.exports = () => run({
      server: task(app.package.scripts['start-watch'], {color: blue}),
      build: task(app.package.scripts['build-watch'], {color: green}),
      lint: task(app.package.scripts['lint-watch'], {color: cyan}),
      test: task(app.package.scripts['test-watch'], {color: yellow})
    })

function run(tasks) {
  Object.keys(tasks)
    .map(name => tasks[name](name, tasks[name]))
}

function task(command, {
  spawn=require('child_process').spawn,
  color
}={}) {
  return name => {
    const stdout = log({name, color}, process.stdout)
        , stderr = log({name, color, text: red}, process.stderr)
        , proc = spawn(command, {
          shell: true,
          stdio: 'pipe',
          env: Object.assign({}, process.env, {
            NODE_ENV: 'development',
            PATH: `${app.root}/node_modules/.bin:${process.env.PATH}`
          })
        }).on('error', error => stderr(error))
          .on('exit', (code, signal) => {
            stderr(`Exited with code ${code}`)
            if (signal) stderr(`Exited with signal ${signal}`)
          })
    proc.stdout.on('data', stdout)
    proc.stderr.on('data', stderr)
  }
}

function log({
  name,
  ts=timestamp,
  color=none,
  text=none,
}, out=process.stdout) {
  return data => data.toString()
    // Strip out screen-clearing control sequences, which really
    // muck up the output.
    .replace('\u001b[2J', '')
    .replace('\u001b[1;3H', '')
    .split('\n')
    .forEach(line => out.write(`${color(`${ts()} ${name}   \t⎹ `)}${text(line)}\n`))
}

const dateformat = require('dateformat')
function timestamp() {
  return dateformat('yyyy-MM-dd HH:MM:ss (Z)')
}

function none(x) { return x }

if (module === require.main) { dev() }
<commit_msg>Cross platform handling of PATH.<commit_after>/**
 * Concurrently run our various dev tasks.
 *
 * Usage: node dev
 **/

const app = require('.')
    , chalk = require('chalk'), {bold} = chalk
    , {red, green, blue, cyan, yellow} = bold
    , dev = module.exports = () => run({
      server: task(app.package.scripts['start-watch'], {color: blue}),
      build: task(app.package.scripts['build-watch'], {color: green}),
      lint: task(app.package.scripts['lint-watch'], {color: cyan}),
      test: task(app.package.scripts['test-watch'], {color: yellow})
    })

function run(tasks) {
  Object.keys(tasks)
    .map(name => tasks[name](name, tasks[name]))
}

function task(command, {
  spawn=require('child_process').spawn,
  path=require('path'),
  color
}={}) {
  return name => {
    const stdout = log({name, color}, process.stdout)
        , stderr = log({name, color, text: red}, process.stderr)
        , proc = spawn(command, {
          shell: true,
          stdio: 'pipe',
          env: Object.assign({}, process.env, {
            NODE_ENV: 'development',
            PATH: [ path.join(app.root, 'node_modules', '.bin')
                  , process.env.PATH ].join(path.delimiter)
          })
        }).on('error', error => stderr(error))
          .on('exit', (code, signal) => {
            stderr(`Exited with code ${code}`)
            if (signal) stderr(`Exited with signal ${signal}`)
          })
    proc.stdout.on('data', stdout)
    proc.stderr.on('data', stderr)
  }
}

function log({
  name,
  ts=timestamp,
  color=none,
  text=none,
}, out=process.stdout) {
  return data => data.toString()
    // Strip out screen-clearing control sequences, which really
    // muck up the output.
    .replace('\u001b[2J', '')
    .replace('\u001b[1;3H', '')
    .split('\n')
    .forEach(line => out.write(`${color(`${ts()} ${name}   \t⎹ `)}${text(line)}\n`))
}

const dateformat = require('dateformat')
function timestamp() {
  return dateformat('yyyy-MM-dd HH:MM:ss (Z)')
}

function none(x) { return x }

if (module === require.main) { dev() }
<|endoftext|>


<commit_before>// cross-browser preventdefault function
function MyPreventDefault(ev){
  if (ev.preventDefault) { ev.preventDefault(); } else { ev.returnValue = false; }
}
<commit_msg>function get delta date in sec,min,day,month,year<commit_after>// cross-browser preventdefault function
function MyPreventDefault(ev){
  if (ev.preventDefault) { ev.preventDefault(); } else { ev.returnValue = false; }
}

// get delta date in sec,min,day,month,year
function getDeltaDate(from, to){
    var dFrom = new Date(from);
    var dTo = new Date(to);

    var year = 0;
    var month = 0;
    var hours = 0;
    var days = 0;
    var minutes = 0;
    var seconds = 0;

    seconds = dTo.getSeconds() - dFrom.getSeconds();
    if( seconds < 0 ){
        seconds += 60;
        minutes--;
    }
    minutes += dTo.getMinutes() - dFrom.getMinutes();
    if( minutes < 0 ){
        minutes += 60;
        hours--;
    }
    hours += dTo.getHours() - dFrom.getHours();
    if( hours < 0 ){
        hours += 24;
        days--;
    }
    days += dTo.getDate() - dFrom.getDate();
    if( days < 0 ){
        days += 30; //посчитать количество дней в месяце From
        month--;
    }
    month += dTo.getMonth() - dFrom.getMonth();
    if( month < 0 ){
        month += 24;
        year--;
    }
    year += dTo.getFullYear() - dFrom.getFullYear();

    var res =
        (
            (year > 0)?
                year+( (year>4)?'л ':'г ' )
                :''
        ) + (
            (month > 0)?
                month+'м '
                :''
        ) + (
            (days > 0)?
                days+'д '
                :''
        ) + hours + ':' + minutes;

    return res;
}
<|endoftext|>


<commit_before>console.log("hello world")
<commit_msg>this is another change<commit_after># this is an example
console.log("hello world")

<|endoftext|>


<commit_before>(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define(factory);
    } else {
        root.crel = factory();
    }
}(this, function () {
    var doc = {},
        document = window.document;

    function isString(thing){
        return typeof thing === 'string';
    }

    function getTarget(target){
        if(isString(target)){
            return doc.find(target)[0];
        }

        return target;
    }

    doc.find = function(target, query){
        if(query == null){
            query = target;
            target = document;
        }
        return getTarget(target).querySelectorAll(query);
    };

    doc.closest = function(target, selector){
        target = getTarget(target);

        if(target !== document && !target.parentNode){
            return null;
        }

        while(
            target &&
            target.parentNode &&
            Array.prototype.slice.apply(isString(selector) ? doc.find(target.parentNode, selector) : selector.parentNode ? selector.parentNode.childNodes : [selector]).indexOf(target) < 0
        ){
            target = target.parentNode;
        }

        return target === document && target !== selector ? null : target;
    };

    doc.is = function(target, query){
        target = getTarget(target);
        return Array.prototype.slice.call(doc.find(target.parentNode, query)).indexOf(target) >= 0;
    };

    doc.addClass = function(target, classes){
        target = getTarget(target);

        var classes = classes.split(' '),
            currentClasses = target.className.split(' ');

        for(var i = 0; i < classes.length; i++){
            var classToAdd = classes[i];
            if(target.classList){
                target.classList.add(classToAdd);
            } else if(!currentClasses.indexOf(classToAdd)>=0){
                currentClasses.push(classToAdd);
            }
        }
        if(!target.classList){
            target.className = currentClasses.join(' ');
        }
        return this;
    };

    doc.removeClass = function(target, classes){
        target = getTarget(target);

        var classes = classes.split(' '),
            currentClasses = target.className.split(' ');

        for(var i = 0; i < classes.length; i++){
            var classToRemove = classes[i];
            if(target.classList){
                target.classList.remove(classToRemove);
                continue;
            }
            var removeIndex = currentClasses.indexOf(classToRemove);
            if(removeIndex >= 0){
                currentClasses.splice(removeIndex, 1);
            }
        }
        if(!target.classList){
            target.className = currentClasses.join(' ');
        }
        return this;
    };

    function addEvent(settings){
        getTarget(settings.target).addEventListener(settings.event, settings.callback, false);
    }

    doc.on = function(events, target, callback, proxy){
        var removeCallbacks = [];

        events = events.split(' ');

        for(var i = 0; i < events.length; i++){
            var eventSettings = [];
            if(proxy){
                if(proxy === true){
                    proxy = document;
                }
                eventSettings.target = proxy;
                eventSettings.callback = function(event){
                    var closestTarget = doc.closest(event.target, target);
                    if(closestTarget){
                        callback(event, closestTarget);
                    }
                };
            }else{
                eventSettings.target = target;
                eventSettings.callback = callback;
            }

            eventSettings.event = events[i];

            addEvent(eventSettings);

            removeCallbacks.push(eventSettings);
        }

        return function(){
            while(removeCallbacks.length){
                var removeCallback = removeCallbacks.pop();

                getTarget(removeCallback.target).removeEventListener(removeCallback.event, getTarget(removeCallback.target));
            }
        }
    };

    doc.isVisible = function(element){
        while(element.parentNode && element.style.display !== 'none'){
            element = element.parentNode;
        }

        return element === document;
    }

    return doc;
}));<commit_msg>Fixed closest<commit_after>(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define(factory);
    } else {
        root.crel = factory();
    }
}(this, function () {
    var doc = {},
        document = window.document;

    function isString(thing){
        return typeof thing === 'string';
    }

    function getTarget(target){
        if(isString(target)){
            return doc.find(target)[0];
        }

        return target;
    }

    doc.find = function(target, query){
        if(query == null){
            query = target;
            target = document;
        }
        return getTarget(target).querySelectorAll(query);
    };

    doc.closest = function(target, query){
        target = getTarget(target);

        while(
            target &&
            target.ownerDocument &&
            !doc.is(target, query) &&
            !(typeof query === 'string' ? doc.find(target, query).length : Array.prototype.slice.call(target.childNodes).indexOf(query)>=0)
        ){
            target = target.parentNode;
        }

        return target === document && target !== query ? null : target;
    };

    doc.is = function(target, query){
        target = getTarget(target);
        if(!target.ownerDocument || typeof query !== 'string'){
            return target === query;
        }
        return target === query || Array.prototype.slice.call(doc.find(target.parentNode, query)).indexOf(target) >= 0;
    };

    doc.addClass = function(target, classes){
        target = getTarget(target);

        var classes = classes.split(' '),
            currentClasses = target.className.split(' ');

        for(var i = 0; i < classes.length; i++){
            var classToAdd = classes[i];
            if(target.classList){
                target.classList.add(classToAdd);
            } else if(!currentClasses.indexOf(classToAdd)>=0){
                currentClasses.push(classToAdd);
            }
        }
        if(!target.classList){
            target.className = currentClasses.join(' ');
        }
        return this;
    };

    doc.removeClass = function(target, classes){
        target = getTarget(target);

        var classes = classes.split(' '),
            currentClasses = target.className.split(' ');

        for(var i = 0; i < classes.length; i++){
            var classToRemove = classes[i];
            if(target.classList){
                target.classList.remove(classToRemove);
                continue;
            }
            var removeIndex = currentClasses.indexOf(classToRemove);
            if(removeIndex >= 0){
                currentClasses.splice(removeIndex, 1);
            }
        }
        if(!target.classList){
            target.className = currentClasses.join(' ');
        }
        return this;
    };

    function addEvent(settings){
        getTarget(settings.target).addEventListener(settings.event, settings.callback, false);
    }

    doc.on = function(events, target, callback, proxy){
        var removeCallbacks = [];

        events = events.split(' ');

        for(var i = 0; i < events.length; i++){
            var eventSettings = [];
            if(proxy){
                if(proxy === true){
                    proxy = document;
                }
                eventSettings.target = proxy;
                eventSettings.callback = function(event){
                    var closestTarget = doc.closest(event.target, target);
                    if(closestTarget){
                        callback(event, closestTarget);
                    }
                };
            }else{
                eventSettings.target = target;
                eventSettings.callback = callback;
            }

            eventSettings.event = events[i];

            addEvent(eventSettings);

            removeCallbacks.push(eventSettings);
        }

        return function(){
            while(removeCallbacks.length){
                var removeCallback = removeCallbacks.pop();

                getTarget(removeCallback.target).removeEventListener(removeCallback.event, getTarget(removeCallback.target));
            }
        }
    };

    doc.isVisible = function(element){
        while(element.parentNode && element.style.display !== 'none'){
            element = element.parentNode;
        }

        return element === document;
    }

    return doc;
}));<|endoftext|>


<commit_before>/*
*  $Id$
*  $HeadURL$
*
*  DOM-related stuff and CSS manipulation class
*
*  @author Ilya Lebedev
*  @author $Author$
*  @modified $Date$
*  @version $Rev$
*  @license LGPL
*  @depends helpers.js
*  @depends arrayextensions.js
*/

if (isUndefined(DOM)) var DOM = {};
/**
 *  Performs parent lookup by
 *   - node object: actually it's "is child of" check
 *   - tagname: getParent(el, 'li') == getParent(el, 'tagName', 'LI')
 *   - any node attribute
 *
 *  @param {HTMLElement} el source element
 *  @param {HTMLElement, String} cp DOMNode or string tagname or string attribute name
 *  @param {String} vl optional attribute value
 *  @return {HTMLElement, Null}
 *  @scope public
 */
DOM.getParent = function (el /* : HTMLElement */, cp /* :String, HTMLElement */, vl /* :String */) /* :HTMLElement */ {
  if (el == null) return null;
  else if (el.nodeType == 1 &&
      ((!isUndefined(vl) && el[cp] == vl) ||
       ('string' == typeof cp && DOM.hasTagName(el, cp)) ||
       el == cp)) return el;
  else return arguments.callee(el.parentNode, cp, vl);
};
/**
 *  Calculates the offset for the DOM node from top left corner
 *
 *  @author Matt Kruse
 *  @see http://javascripttoolbox.com/lib/objectposition/index.php
 *  @param {HTMLElement} el
 *  @return {Object} x: horizontal offset, y: vertical offset
 *  @scope public
 */
DOM.getOffset = function (el /* :HTMLElement */) /* :Object */ {
    var fixBrowserQuirks = true
       ,o = el
       ,left = 0
       ,top = 0
       ,width = 0
       ,height = 0
       ,parentNode = null
       ,offsetParent = null;

    if (o==null) return null;

    offsetParent = o.offsetParent;
    var originalObject = o
       ,el = o; // "el" will be nodes as we walk up, "o" will be saved for offsetParent references
    while (el.parentNode!=null) {
      el = el.parentNode;
      if (el.offsetParent==null) {
      }
      else {
        var considerScroll = true;
        /*
        In Opera, if parentNode of the first object is scrollable, then offsetLeft/offsetTop already
        take its scroll position into account. If elements further up the chain are scrollable, their
        scroll offsets still need to be added in. And for some reason, TR nodes have a scrolltop value
        which must be ignored.
        */
        if (fixBrowserQuirks && window.opera) {
          if (el==originalObject.parentNode || el.nodeName=="TR") {
            considerScroll = false;
          }
        }
        if (considerScroll) {
          if (el.scrollTop && el.scrollTop>0) {
            top -= el.scrollTop;
          }
          if (el.scrollLeft && el.scrollLeft>0) {
            left -= el.scrollLeft;
          }
        }
      }
      // If this node is also the offsetParent, add on the offsets and reset to the new offsetParent
      if (el == offsetParent) {
        left += o.offsetLeft;
        if (el.clientLeft && el.nodeName!="TABLE") {
          left += el.clientLeft;
        }
        top += o.offsetTop;
        if (el.clientTop && el.nodeName!="TABLE") {
          top += el.clientTop;
        }
        o = el;
        if (o.offsetParent==null) {
          if (o.offsetLeft) {
            left += o.offsetLeft;
          }
          if (o.offsetTop) {
            top += o.offsetTop;
          }
        }
        offsetParent = o.offsetParent;
      }
    }

    if (originalObject.offsetWidth) {
      width = originalObject.offsetWidth;
    }
    if (originalObject.offsetHeight) {
      height = originalObject.offsetHeight;
    }

    return {'x':left, 'y':top, 'width':width, 'height':height};
};

//DOM.getOffset = function (el /* :HTMLElement */) /* :Object */ {
/*
    var xy = {'x' : el.offsetLeft , 'y' : el.offsetTop};
    if (el.offsetParent) {
        var xy1 = arguments.callee(el.offsetParent);
        xy.x += xy1.x;
        xy.y += xy1.y;
    }
    return xy;
}
*/
/**
 *  Returns the width of the window canvas
 *
 *  @return {Number}
 *  @scope public
 */
DOM.getClientWidth = function () /* :Number */{
    var w=0;
    if (self.innerHeight) w = self.innerWidth;
    else if (document.documentElement && document.documentElement.clientWidth) w = document.documentElement.clientWidth;
    else if (document.body) w = document.body.clientWidth;
    return w;
};
/**
 *  Returns the height of the window canvas
 *
 *  @return {Number}
 *  @scope public
 */
DOM.getClientHeight = function () /* :Number */{
    var h=0;
    if (self.innerHeight) h = self.innerHeight;
    else if (document.documentElement && document.documentElement.clientHeight) h = document.documentElement.clientHeight;
    else if (document.body) h = document.body.clientHeight;
    return h;
};
/**
 *  Returns the height of the scrolled area for the body
 *
 *  @return {Number}
 *  @scope public
 */
DOM.getBodyScrollTop = function () /* :Number */{
    return self.pageYOffset || (document.documentElement && document.documentElement.scrollTop) || (document.body && document.body.scrollTop);
};
/**
 *  Returns the height of the scrolled area for the body
 *
 *  @return {Number}
 *  @scope public
 */
DOM.getBodyScrollLeft = function () /* :Number */{
    return self.pageXOffset || (document.documentElement && document.documentElement.scrollLeft) || (document.body && document.body.scrollLeft);
};
/**
 *  Calculates cursor position properly
 *
 *  @param {Event} e event object to get cursor positions from
 *  @return {Object} object with x and y cursor positions
 *  @scope protected
 *  @see http://hartshorne.ca/2006/01/23/javascript_cursor_position/
 *  @author Beau Hartshorne
 */
DOM.getCursorPosition = function (e) {
    if (e.pageX || e.pageY) return {'x': e.pageX, 'y': e.pageY};

    var de = document.documentElement || document.body;
    return {'x': e.clientX + de.scrollLeft - (de.clientLeft || 0)
           ,'y': e.clientY + de.scrollTop - (de.clientTop || 0)};
};
/**
 *  Checks, if property matches a tagname(s)
 *
 *  @param {HTMLElement} prop
 *  @param {String, Array} tags
 *  @return {Boolean}
 *  @scope public
 */
DOM.hasTagName = function (prop /* :HTMLElement */, tags /* :String, Array */) {
    if (isString(tags)) tags = [tags];
    if (!isArray(tags) || isEmpty(tags) || isUndefined(prop) || isEmpty(prop.tagName)) return false;
    var t = prop.tagName.toLowerCase();
    for (var i=0, tL=tags.length; i<tL; i++) {
        if (tags[i].toLowerCase() == t) return true;
    }
    return false;
};
/**
 *  Return the actual rgb color value from the following formats
 *  #rrggbb
 *  #rgb
 *  rgb (0..255, 0..255,0..255)
 *  rgb (0..100%, 0..100%,0..100%)
 *  <color_name>
 *
 *  @param {String} from attr name
 *  @return {Array} r,g,b values
 *  @scope public
 */
DOM.color2rgb = function (prop) {
     var e;
     /*
     *  note, properties like borderColor might have the series of colors
     */
     if (/^([a-z]+)($|\s[a-z]+)/i.test(prop)) {
         var d = document.body, ov = d.vLink;
         d.vLink = prop.split(" ")[0];
         prop = d.vLink;
         d.vLink = ov;
     }
     try {
     if (e = prop.match(/^#([\da-f]{6})$/i))
         return e=parseInt(e[1],16),[(e&0xff0000)>>16,(e&0xff00)>>8,(e&0xff)]
     else if (e = prop.match(/^#([\da-f]{3})$/i)) {
         return e=parseInt(e[1],16),[((e&0xf00)>>8)*0x11,((e&0xf0)>>4)*0x11,(e&0xf)*0x11];
     } else
         return (prop.match(/([\d%]+)/g).splice(0,3).map(function(a){ return /%/.test(a)?(parseInt(a)*2.55).toFixed(0):parseInt(a)}))
     } catch(err){
     }
}
/**
 *  DOM.CSS is the CSS processing class, allowing to easy mangle class names
 *
 *  @param {HTMLElement} el element to provide interface for
 *  @scope public
 *  @constructor
 *  @class DOM.CSS
 *  @exception on invalid parameter
 *  @depends arrayextensions.js
 *  @depends helpers.js
 */
DOM.CSS = function (el) {
    var self = this
    /**
     *  Adds the class name, unlimited number of arguments is supported
     *
     *  @param {String} class classname to apply to the element
     *  @return {Object} singleton object to chain operations
     *  @scope public
     */
    self.addClass = function() {
        var css = el.className
           ,arg = isArray(arguments[0])?arguments[0]:arguments;
        for (var i=arg.length-1, ar, re; i>=0; i--) {
            ar=arg[i];
            re=new RegExp("(^|\\s+)"+ar+"(\\s+|$)");
            if (!css.match(re))
                css += " "+ar;
        }
        el.className = css;
        return self;
    };
    /**
     *  Removes the class name, unlimited number of arguments is supported
     *
     *  @param {String} class classname to apply to the element
     *  @return {Object} singleton object to chain operations
     *  @scope public
     */
    self.removeClass = function() {
        var css = el.className
           ,arg = isArray(arguments[0])?arguments[0]:arguments;
        for (var i=arg.length-1, ar, re, f=false; i>=0; i--) {
            ar=arg[i];
            re=new RegExp("(^|\\s+)"+ar+"(\\s+|$)");
            css = css.replace(re," ");
        }
        el.className = css;
        return self;
    };
    /**
     *  Checks classname for the certain class
     *
     *  @param {String} c class name to check for
     *  @return {Boolean} class name existence
     *  @scope public
     */
    self.hasClass = function(c) {
        re=new RegExp("(^|\\s+)"+c+"(\\s+|$)");
        return el.className.match(re," "+c+" ");
    };
    /**
     *  Returns the actual CSS class for the element
     *
     *  @return {String} css class
     *  @scope public
     */
    self.getClass = function() {
        return el.className;
    }
    /**
     *  Retrieves class value from class name by pattern
     *   class-var = "name:value"
     *   name = [a-z][-a-z0-9]
     *   value = value | val1:val2:...:valN
     *
     *  @param {String} c class name to check for
     *  @return {String, Array} value(s)
     *  @scope public
     */
    self.getClassValue = function(c) {
        var vals = el.className.match(new RegExp("(^|\\s)"+c+":([^\\s]+)"));

        return vals?((vals[2].indexOf(":")+1)?vals[2].split(":")
                                             :vals[2])
                   :null;
    };
    /**
     *  Returns actual style for the element, computed from CSS and inline styles
     *
     *  @param {String} prop optional style property to fetch
     *  @return {Object} computed style or property value
     *  @scope public
     */
    self.getComputedStyle = function(prop) {
        var y;
        if (el.currentStyle)
            y = prop?el.currentStyle[prop]:x.currentStyle;
        else if (window.getComputedStyle) {
            y = document.defaultView.getComputedStyle(el,null);
            if (prop) y=y.getPropertyValue(prop)
        } else {
            y = null;
        }
        return y;
    }
    return this;
};
<commit_msg>% tuned DOM.CSS performance<commit_after>/*
*  $Id$
*  $HeadURL$
*
*  DOM-related stuff and CSS manipulation class
*
*  @author Ilya Lebedev
*  @author $Author$
*  @modified $Date$
*  @version $Rev$
*  @license LGPL
*  @depends helpers.js
*  @depends arrayextensions.js
*/

if (isUndefined(DOM)) var DOM = {};
/**
 *  Performs parent lookup by
 *   - node object: actually it's "is child of" check
 *   - tagname: getParent(el, 'li') == getParent(el, 'tagName', 'LI')
 *   - any node attribute
 *
 *  @param {HTMLElement} el source element
 *  @param {HTMLElement, String} cp DOMNode or string tagname or string attribute name
 *  @param {String} vl optional attribute value
 *  @return {HTMLElement, Null}
 *  @scope public
 */
DOM.getParent = function (el /* : HTMLElement */, cp /* :String, HTMLElement */, vl /* :String */) /* :HTMLElement */ {
  if (el == null) return null;
  else if (el.nodeType == 1 &&
      ((!isUndefined(vl) && el[cp] == vl) ||
       ('string' == typeof cp && DOM.hasTagName(el, cp)) ||
       el == cp)) return el;
  else return arguments.callee(el.parentNode, cp, vl);
};
/**
 *  Calculates the offset for the DOM node from top left corner
 *
 *  @author Matt Kruse
 *  @see http://javascripttoolbox.com/lib/objectposition/index.php
 *  @param {HTMLElement} el
 *  @return {Object} x: horizontal offset, y: vertical offset
 *  @scope public
 */
DOM.getOffset = function (el /* :HTMLElement */) /* :Object */ {
    var fixBrowserQuirks = true
       ,o = el
       ,left = 0
       ,top = 0
       ,width = 0
       ,height = 0
       ,parentNode = null
       ,offsetParent = null;

    if (o==null) return null;

    offsetParent = o.offsetParent;
    var originalObject = o
       ,el = o; // "el" will be nodes as we walk up, "o" will be saved for offsetParent references
    while (el.parentNode!=null) {
      el = el.parentNode;
      if (el.offsetParent==null) {
      }
      else {
        var considerScroll = true;
        /*
        In Opera, if parentNode of the first object is scrollable, then offsetLeft/offsetTop already
        take its scroll position into account. If elements further up the chain are scrollable, their
        scroll offsets still need to be added in. And for some reason, TR nodes have a scrolltop value
        which must be ignored.
        */
        if (fixBrowserQuirks && window.opera) {
          if (el==originalObject.parentNode || el.nodeName=="TR") {
            considerScroll = false;
          }
        }
        if (considerScroll) {
          if (el.scrollTop && el.scrollTop>0) {
            top -= el.scrollTop;
          }
          if (el.scrollLeft && el.scrollLeft>0) {
            left -= el.scrollLeft;
          }
        }
      }
      // If this node is also the offsetParent, add on the offsets and reset to the new offsetParent
      if (el == offsetParent) {
        left += o.offsetLeft;
        if (el.clientLeft && el.nodeName!="TABLE") {
          left += el.clientLeft;
        }
        top += o.offsetTop;
        if (el.clientTop && el.nodeName!="TABLE") {
          top += el.clientTop;
        }
        o = el;
        if (o.offsetParent==null) {
          if (o.offsetLeft) {
            left += o.offsetLeft;
          }
          if (o.offsetTop) {
            top += o.offsetTop;
          }
        }
        offsetParent = o.offsetParent;
      }
    }

    if (originalObject.offsetWidth) {
      width = originalObject.offsetWidth;
    }
    if (originalObject.offsetHeight) {
      height = originalObject.offsetHeight;
    }

    return {'x':left, 'y':top, 'width':width, 'height':height};
};

//DOM.getOffset = function (el /* :HTMLElement */) /* :Object */ {
/*
    var xy = {'x' : el.offsetLeft , 'y' : el.offsetTop};
    if (el.offsetParent) {
        var xy1 = arguments.callee(el.offsetParent);
        xy.x += xy1.x;
        xy.y += xy1.y;
    }
    return xy;
}
*/
/**
 *  Returns the width of the window canvas
 *
 *  @return {Number}
 *  @scope public
 */
DOM.getClientWidth = function () /* :Number */{
    var w=0;
    if (self.innerHeight) w = self.innerWidth;
    else if (document.documentElement && document.documentElement.clientWidth) w = document.documentElement.clientWidth;
    else if (document.body) w = document.body.clientWidth;
    return w;
};
/**
 *  Returns the height of the window canvas
 *
 *  @return {Number}
 *  @scope public
 */
DOM.getClientHeight = function () /* :Number */{
    var h=0;
    if (self.innerHeight) h = self.innerHeight;
    else if (document.documentElement && document.documentElement.clientHeight) h = document.documentElement.clientHeight;
    else if (document.body) h = document.body.clientHeight;
    return h;
};
/**
 *  Returns the height of the scrolled area for the body
 *
 *  @return {Number}
 *  @scope public
 */
DOM.getBodyScrollTop = function () /* :Number */{
    return self.pageYOffset || (document.documentElement && document.documentElement.scrollTop) || (document.body && document.body.scrollTop);
};
/**
 *  Returns the height of the scrolled area for the body
 *
 *  @return {Number}
 *  @scope public
 */
DOM.getBodyScrollLeft = function () /* :Number */{
    return self.pageXOffset || (document.documentElement && document.documentElement.scrollLeft) || (document.body && document.body.scrollLeft);
};
/**
 *  Calculates cursor position properly
 *
 *  @param {Event} e event object to get cursor positions from
 *  @return {Object} object with x and y cursor positions
 *  @scope protected
 *  @see http://hartshorne.ca/2006/01/23/javascript_cursor_position/
 *  @author Beau Hartshorne
 */
DOM.getCursorPosition = function (e) {
    if (e.pageX || e.pageY) return {'x': e.pageX, 'y': e.pageY};

    var de = document.documentElement || document.body;
    return {'x': e.clientX + de.scrollLeft - (de.clientLeft || 0)
           ,'y': e.clientY + de.scrollTop - (de.clientTop || 0)};
};
/**
 *  Checks, if property matches a tagname(s)
 *
 *  @param {HTMLElement} prop
 *  @param {String, Array} tags
 *  @return {Boolean}
 *  @scope public
 */
DOM.hasTagName = function (prop /* :HTMLElement */, tags /* :String, Array */) {
    if (isString(tags)) tags = [tags];
    if (!isArray(tags) || isEmpty(tags) || isUndefined(prop) || isEmpty(prop.tagName)) return false;
    var t = prop.tagName.toLowerCase();
    for (var i=0, tL=tags.length; i<tL; i++) {
        if (tags[i].toLowerCase() == t) return true;
    }
    return false;
};
/**
 *  Return the actual rgb color value from the following formats
 *  #rrggbb
 *  #rgb
 *  rgb (0..255, 0..255,0..255)
 *  rgb (0..100%, 0..100%,0..100%)
 *  <color_name>
 *
 *  @param {String} from attr name
 *  @return {Array} r,g,b values
 *  @scope public
 */
DOM.color2rgb = function (prop) {
     var e;
     /*
     *  note, properties like borderColor might have the series of colors
     */
     if (/^([a-z]+)($|\s[a-z]+)/i.test(prop)) {
         var d = document.body, ov = d.vLink;
         d.vLink = prop.split(" ")[0];
         prop = d.vLink;
         d.vLink = ov;
     }
     try {
     if (e = prop.match(/^#([\da-f]{6})$/i))
         return e=parseInt(e[1],16),[(e&0xff0000)>>16,(e&0xff00)>>8,(e&0xff)]
     else if (e = prop.match(/^#([\da-f]{3})$/i)) {
         return e=parseInt(e[1],16),[((e&0xf00)>>8)*0x11,((e&0xf0)>>4)*0x11,(e&0xf)*0x11];
     } else
         return (prop.match(/([\d%]+)/g).splice(0,3).map(function(a){ return /%/.test(a)?(parseInt(a)*2.55).toFixed(0):parseInt(a)}))
     } catch(err){
     }
}
/**
 *  DOM.CSS is the CSS processing class, allowing to easy mangle class names
 *
 *  @param {HTMLElement} el element to provide interface for
 *  @scope public
 *  @constructor
 *  @class DOM.CSS
 *  @exception on invalid parameter
 *  @depends arrayextensions.js
 *  @depends helpers.js
 */
DOM.CSS = function (el) {
    var self = this
    /**
     *  Adds the class name, unlimited number of arguments is supported
     *
     *  @param {String} class classname to apply to the element
     *  @return {Object} singleton object to chain operations
     *  @scope public
     */
    self.addClass = function() {
        var arg = isArray(arguments[0])?arguments[0]:Array.prototype.slice.call(arguments);
        self.removeClass(arg);
        el.className = el.className+" "+Array.prototype.join.call(arg," ");
        return self;
    };
    /**
     *  Removes the class name, unlimited number of arguments is supported
     *
     *  @param {String} class classname to apply to the element
     *  @return {Object} singleton object to chain operations
     *  @scope public
     */
    self.removeClass = function() {
        var arg = Array.prototype.join.call((isArray(arguments[0])?arguments[0]:arguments),"|");
        if (!arguments.callee.cache) arguments.callee.cache = {}
        var c = arguments.callee.cache
        if (!c.hasOwnProperty(arg)) c[arg] = new RegExp("(^|\\s+)("+arg+")(\\s+|$)","g");
        el.className = el.className.replace(c[arg]," ");
        return self;
    };
    /**
     *  Checks classname for the certain class
     *
     *  @param {String} c class name to check for
     *  @return {Boolean} class name existence
     *  @scope public
     */
    self.hasClass = function(c) {
        re=new RegExp("(^|\\s+)"+c+"(\\s+|$)");
        return el.className.match(re," "+c+" ");
    };
    /**
     *  Returns the actual CSS class for the element
     *
     *  @return {String} css class
     *  @scope public
     */
    self.getClass = function() {
        return el.className;
    }
    /**
     *  Retrieves class value from class name by pattern
     *   class-var = "name:value"
     *   name = [a-z][-a-z0-9]
     *   value = value | val1:val2:...:valN
     *
     *  @param {String} c class name to check for
     *  @return {String, Array} value(s)
     *  @scope public
     */
    self.getClassValue = function(c) {
        var vals = el.className.match(new RegExp("(^|\\s)"+c+":([^\\s]+)"));

        return vals?((vals[2].indexOf(":")+1)?vals[2].split(":")
                                             :vals[2])
                   :null;
    };
    /**
     *  Returns actual style for the element, computed from CSS and inline styles
     *
     *  @param {String} prop optional style property to fetch
     *  @return {Object} computed style or property value
     *  @scope public
     */
    self.getComputedStyle = function(prop) {
        var y;
        if (el.currentStyle)
            y = prop?el.currentStyle[prop]:x.currentStyle;
        else if (window.getComputedStyle) {
            y = document.defaultView.getComputedStyle(el,null);
            if (prop) y=y.getPropertyValue(prop)
        } else {
            y = null;
        }
        return y;
    }
    return this;
};
<|endoftext|>


<commit_before>/*// Called when the url of a tab changes.
function checkForValidUrl(tabId, changeInfo, tab) {
  // If the letter 'g' is found in the tab's URL...
  if (tab.url.indexOf("https://courses2013.dalton.org/") > -1) {
    // ... show the page action.
    chrome.pageAction.show(tabId);
  }
};

// Listen for any changes to the URL of any tab.
chrome.tabs.onUpdated.addListener(checkForValidUrl);*/
console.log("Courses+ background page!");
chrome.webRequest.onBeforeSendHeaders.addListener(function(details) {
	if (details.url.indexOf("user/icon/dalton/") > -1) {
		details.requestHeaders.push({name: 'X-CoursesPlus-RefererSpoof-Yay', value: "https://courses2015.dalton.org/user/profile.php"});
	}
	for (var i = 0; i < details.requestHeaders.length; ++i) {
		if (details.requestHeaders[i].name === 'X-CoursesPlus-RefererSpoof-Yay') {
			var newVal = details.requestHeaders[i].value;
			//details.requestHeaders.splice(i, 1);
			for (var j = 0; j < details.requestHeaders.length; ++j) {
				if (details.requestHeaders[i].name === 'Referer') {
					break;
				}
			}
			if (details.requestHeaders[j] !== 'Referer') {
				details.requestHeaders.push({name: 'Referer', value: newVal});
			} else {
				details.requestHeaders[j].value = newVal;
			}
			break;
		}
	}
	return {requestHeaders: details.requestHeaders};
}, {urls: ["*://courses2015.dalton.org/*"]}, ["blocking", "requestHeaders"]);<commit_msg>Remove unused code<commit_after>console.log("Courses+ background page!");
chrome.webRequest.onBeforeSendHeaders.addListener(function(details) {
	if (details.url.indexOf("user/icon/dalton/") > -1) {
		details.requestHeaders.push({name: 'X-CoursesPlus-RefererSpoof-Yay', value: "https://courses2015.dalton.org/user/profile.php"});
	}
	for (var i = 0; i < details.requestHeaders.length; ++i) {
		if (details.requestHeaders[i].name === 'X-CoursesPlus-RefererSpoof-Yay') {
			var newVal = details.requestHeaders[i].value;
			//details.requestHeaders.splice(i, 1);
			for (var j = 0; j < details.requestHeaders.length; ++j) {
				if (details.requestHeaders[i].name === 'Referer') {
					break;
				}
			}
			if (details.requestHeaders[j] !== 'Referer') {
				details.requestHeaders.push({name: 'Referer', value: newVal});
			} else {
				details.requestHeaders[j].value = newVal;
			}
			break;
		}
	}
	return {requestHeaders: details.requestHeaders};
}, {urls: ["*://courses2015.dalton.org/*"]}, ["blocking", "requestHeaders"]);<|endoftext|>


<commit_before>module.exports = {
    rules: {
        'arrow-body-style': [2, 'as-needed'],
        'arrow-parens': [2, 'as-needed'],
        'arrow-spacing': [2, {
            before: true,
            after: true
        }],
        'no-confusing-arrow': [2],
        'no-useless-computed-key': [2],
        'no-useless-constructor': [2],
        'no-var': [1],
        'prefer-arrow-callback': [2],
        'prefer-const': [1, {destructuring: 'all'}],
        'prefer-template': [2],
        'rest-spread-spacing': [2, 'never'],
        'template-curly-spacing': [2, 'never']
    },
    env: {
        es6: true
    }
};
<commit_msg>fix(es6): Allow rest/spread syntax<commit_after>module.exports = {
    rules: {
        'arrow-body-style': [2, 'as-needed'],
        'arrow-parens': [2, 'as-needed'],
        'arrow-spacing': [2, {
            before: true,
            after: true
        }],
        'no-confusing-arrow': [2],
        'no-useless-computed-key': [2],
        'no-useless-constructor': [2],
        'no-var': [1],
        'prefer-arrow-callback': [2],
        'prefer-const': [1, {destructuring: 'all'}],
        'prefer-template': [2],
        'rest-spread-spacing': [2, 'never'],
        'template-curly-spacing': [2, 'never']
    },
    env: {
        es6: true
    },
    parserOptions: {
        ecmaFeatures: {
            experimentalObjectRestSpread: true
        }
    }
};
<|endoftext|>


<commit_before>var $ = require('interlude')
  , Base = require('tournament')
  , algs = require('./balancer');

// ffa has no concepts of sections yet so they're all 1
var idString = function (id) {
  if (!id.m) {
    return "R" + id.r + " M X";
  }
  return "R" + id.r + " M" + id.m;
};

var unspecify = function (grps) {
  return grps.map(function (grp) {
    return $.replicate(grp.length, Base.NONE);
  });
};

var roundInvalid = function (np, grs, adv, numGroups) {
  // the group size in here refers to the maximal reduced group size
  if (Math.ceil(grs) !== grs || Math.ceil(adv) !== adv) {
    return "individual group size and adv must all be integers";
  }
  if (Math.ceil(np) !== np || np < 2) {
    return "needs at least 2 players";
  }
  if (grs < 3 || (numGroups === 1 && grs >= 2)) {
    return "groups size must be at least 3 in regular rounds - 2 in final";
  }
  if (grs >= np) {
    return "group size must be less than the number of players left";
  }
  if (adv >= grs) {
    return "must advance less than the group size";
  }
  var isUnfilled = (np % numGroups) > 0;
  if (isUnfilled && adv >= grs - 1) {
    return "must advance less than the smallest match size";
  }
  if (adv <= 0) {
    return "must eliminate players each match";
  }
  return null;
};

var finalInvalid = function (leftOver, limit, gLast) {
  if (leftOver < 2) {
    return "must at least contain 2 players"; // force >4 when using limits
  }
  var lastNg = Math.ceil(leftOver / gLast);
  if (limit > 0) { // using limits
    if (limit >= leftOver) {
      return "limit must be less than the remaining number of players";
    }
    // need limit to be a multiple of numGroups (otherwise tiebreaks necessary)
    if (limit % lastNg !== 0) {
      return "number of groups must divide limit";
    }
  }
  else if (lastNg !== 1) {
    return "must contain a single match when not using limits";
  }
  return null;
};

var invalid = function (np, grs, adv, opts) {
  if (!Number.isFinite(np) || Math.ceil(np) !== np || np < 2) {
    return "number of players must be at least 2";
  }
  if (!Array.isArray(grs) || !Array.isArray(adv)) {
    return "adv and grs must be arrays";
  }
  if (!grs.length || !grs.every(Number.isFinite)) {
    return "grs must be a non-empty array of integers";
  }
  if (!adv.every(Number.isFinite) || grs.length !== adv.length + 1) {
    return "adv must be an array of integers of length grs.length - 1";
  }

  var numGroups = 0;
  for (var i = 0; i < adv.length; i += 1) {
    // loop over adv as then both a and g exist
    var a = adv[i];
    var g = grs[i];
    // calculate how big the groups are
    numGroups = Math.ceil(np / g);
    var gActual = algs.reduceGroupSize(np, g, numGroups);

    // and ensure with group reduction that eliminationValid for reduced params
    var invReason = roundInvalid(np, gActual, a, numGroups);
    if (invReason !== null) {
      return "round " + (i+1) + " " + invReason;
    }
    // return how many players left so that np is updated for next itr
    np = numGroups*a;
  }
  // last round and limit checks
  var invFinReason = finalInvalid(np, (opts && opts.limit) | 0, grs[grs.length-1]);
  if (invFinReason !== null) {
    return "final round: " + invFinReason;
  }

  // nothing found - ok to create
  return null;
};

var elimination = function (np, grs, adv, limit) {
  var invReason = invalid(np, grs, adv, {limit: limit});
  if (invReason !== null) {
    console.error("Invalid FFA configuration %dp sizes=%j, advs=%j, opts=%j"
      , np, grs, adv, {limit: limit});
    console.error("reason: %s", invReason);
    return {};
  }
  //console.log('creating %dp FFA elimination (%j/%j advancing)', np, adv, grs);

  var matches = []; // pushed in sort order
  // rounds created iteratively - know configuration valid at this point so just
  // repeat the calculation in the validation
  for (var i = 0; i < grs.length; i += 1) {
    var a = adv[i]
      , gs = grs[i]
      , numGroups = Math.ceil(np / gs)
      , gsActual = algs.reduceGroupSize(np, gs, numGroups);

    // irrelevant which gs we use as algs.groups reduces anyway
    // though might as well save it the effort and we need it here anyway
    var grps = algs.groups(np, gsActual);
    if (numGroups !== grps.length) {
      throw new Error("internal FFA construction error");
    }
    if (i > 0) {
      // only fill in seeding numbers for round 1, otherwise placeholders
      grps = unspecify(grps);
    }

    // fill in matches
    for (var m = 0; m < grps.length; m += 1) {
      matches.push({id: {s: 1, r: i+1, m: m + 1}, p: grps[m]}); // matches 1-indexed
    }
    // reduce players left (for next round - which will exist if a is defined)
    np = numGroups*a;
  }
  return matches;
};

// interface
function FFA(numPlayers, grs, advs, opts) {
  if (!(this instanceof FFA)) {
    return new FFA(numPlayers, grs, advs, opts);
  }
  this.version = 1;
  this.advs = advs;
  this.limit = opts ? opts.limit | 0 : 0;
  this.numPlayers = numPlayers;
  Base.call(this, elimination(numPlayers, grs, advs, this.limit));
}
FFA.prototype = Object.create(Base.prototype);
FFA.parse = Base.parse.bind(null, FFA);
FFA.invalid = invalid;
FFA.idString = idString;

FFA.prototype.unscorable = function (id, score, allowPast) {
  var invReason = Base.prototype.unscorable.call(this, id, score, allowPast);
  if (invReason !== null) {
    return invReason;
  }
  var adv = this.advs[id.r - 1] || 0;
  if (adv > 0 && score[adv] === score[adv - 1]) {
    return "scores must unambiguous decide who advances";
  }
  if (!adv && this.limit > 0) {
    // number of groups in last round is the match number of the very last match
    // because of the ordering this always works!
    var lastNG = this.matches[this.matches.length-1].id.m;
    var cutoff = this.limit/lastNG; // NB: lastNG divides limit (from finalInvalid)
    if (score[cutoff] === score[cutoff - 1]) {
      return "scores must decide who advances in final round with limits";
    }
  }
  return null;
};

var prepRound = function (currRnd, nxtRnd, adv) {
  var top = currRnd.map(function (m) {
    return $.zip(m.p, m.m).sort(Base.compareZip).slice(0, adv);
  });

  // now flatten and sort across matches
  // this essentially re-seeds players for the next round
  top = $.pluck(0, $.flatten(top).sort(Base.compareZip));

  // re-find group size from maximum length of zeroed player array in next round
  var grs = $.maximum($.pluck('length', $.pluck('p', nxtRnd)));

  // set all next round players with the fairly grouped set
  algs.groups(top.length, grs).forEach(function (group, k) {
    // replaced nulled out player array with seeds mapped to corr. top placers
    nxtRnd[k].p = group.map(function (seed) {
      return top[seed-1]; // NB: top is zero indexed
    });
  });
};

// updates ms in place and returns whether or not anything changed
FFA.prototype.score = function (id, score) {
  if (Base.prototype.score.call(this, id, score)) {
    var adv = this.advs[id.r - 1] || 0;
    var currRnd = this.findMatches({r: id.r});
    if (currRnd.every($.get('m')) && adv > 0) {
      prepRound(currRnd, this.findMatches({r: id.r + 1}), adv);
    }
    return true;
  }
  return false;
};

FFA.prototype.upcoming = function (playerId) {
  var id = Base.prototype.upcoming.call(this, playerId);
  if (id) {
    return id; // player not waiting for new rounds - match ready
  }

  // if player reached currentRound, he may be waiting for generation of nextRound
  var m = $.firstBy(function (m) {
    return m.p.indexOf(playerId) >= 0 && m.m;
  }, this.currentRound() || []);

  if (m) {
    // will he advance to nextRound ?
    var adv = this.advs[m.id.r - 1];
    if (Base.sorted(m).slice(0, adv).indexOf(playerId) >= 0) {
      return {s: 1, r: m.id.r + 1};
    }
  }
};

// helpers for results' round loop
var isReady = function (rnd) {
  return rnd.some(function (m) {
    return m.p.some($.neq(Base.NONE));
  });
};
var isDone = function (rnd) {
  return rnd.every($.get('m'));
};

FFA.prototype.results = function () {
  // TODO: best scores?
  var res = Base.prototype.results.call(this, { sum: 0 });
  var advs = this.advs;
  var maxround = 1;
  for (var i = 0; i < this.matches.length; i += 1) {
    var g = this.matches[i];
    maxround = Math.max(g.id.r, maxround);

    if (g.m) {
      // count stats for played matches
      var top = $.zip(g.p, g.m).sort(Base.compareZip);
      for (var j = 0; j < top.length; j += 1) {
        var pJ = top[j][0] - 1  // convert seed -> zero indexed player number
          , mJ = top[j][1];     // map wins for pJ

        var adv = advs[g.id.r - 1] || 0;
        // NB: final round win counted by .positionTies as can have multiple winners
        if (j < adv) {
          res[pJ].wins += 1;
        }
        res[pJ].sum += mJ;
      }
    }
  }

  var limit = this.limit;
  // gradually improve scores for each player by looking at later and later rounds
  this.rounds().forEach(function (rnd, k) {
    var adv = advs[k] || 0; // so we can do last round
    var rndPs = $.flatten($.pluck('p', rnd));

    if (isDone(rnd)) {
      if (limit > 0 && k === maxround - 1) {
        // this is the special case of a `limit`ed tournament
        // it may not be a 'final' in the sense that rnd.length >= 1
        // as the winnners goto a new tournament and losers are knocked out

        if (rndPs.length < limit) {
          // sanity check for own tests
          throw new Error("FFA internal error - too few players for forwarding");
        }
        // figure out how many to advance from each round
        // we know limit is a multiple of rnd.length
        if (limit % rnd.length !== 0) {
          throw new Error("FFA internal error - limit not multiple of maxrnd len");
        }
        adv = limit / rnd.length;

        // losers fall through and are scored as any other done round with 'adv' set
        // but we must positions winners as well (top adv each match) in this rnd
        // as nothing else does this in this special case
        rnd.forEach(function (m) {
          // loop through winners
          Base.sorted(m).slice(0, adv).forEach(function (w, i) {
            var resEl = res[w - 1];
            // no adv set for this round so must also increment wins for these
            resEl.wins += 1;
            // their final position shall be tied between groups, and desc within
            resEl.pos = i*rnd.length + 1;
          });
        });
      }
      // if round is done, position the ones not advancing from that round
      // collect and sort between round the ones after top adv
      // NB: in `limit`-less final, adv is zero, so everyone's treated as a loser
      rnd.forEach(function (m) {
        // start at: numPlayers - (numPlayers - adv*numGroups) + 1
        var start = rndPs.length - (rndPs.length - adv*rnd.length) + 1;

        // loop through losers
        Base.sorted(m).slice(adv).forEach(function (l, i) {
          var resEl = res[l - 1];
          if (i === 0 && adv === 0) {
            // set wins +1 on actual winner in `limit`-less final
            resEl.wins += 1;
          }
          // tie x-placing losers between groups (at least pos-wise)
          resEl.pos = start + i*rnd.length;
        });
      });
    }
    else if (isReady(rnd)) {
      // round is simply ready, make sure everyone who got here is tied
      rndPs.forEach(function (p) {
        // last place in round is simply number of players beneath
        res[p-1].pos = rndPs.length;
      });
    }
  });
  // still sort also by sum in case people want to use that
  return res.sort($.comparing('pos', +1, 'sum', -1));
};

module.exports = FFA;
<commit_msg>fix tests for new tournament<commit_after>var $ = require('interlude')
  , Base = require('tournament')
  , algs = require('./balancer');

// ffa has no concepts of sections yet so they're all 1
var idString = function (id) {
  if (!id.m) {
    return "R" + id.r + " M X";
  }
  return "R" + id.r + " M" + id.m;
};

var unspecify = function (grps) {
  return grps.map(function (grp) {
    return $.replicate(grp.length, Base.NONE);
  });
};

var roundInvalid = function (np, grs, adv, numGroups) {
  // the group size in here refers to the maximal reduced group size
  if (Math.ceil(grs) !== grs || Math.ceil(adv) !== adv) {
    return "individual group size and adv must all be integers";
  }
  if (Math.ceil(np) !== np || np < 2) {
    return "needs at least 2 players";
  }
  if (grs < 3 || (numGroups === 1 && grs >= 2)) {
    return "groups size must be at least 3 in regular rounds - 2 in final";
  }
  if (grs >= np) {
    return "group size must be less than the number of players left";
  }
  if (adv >= grs) {
    return "must advance less than the group size";
  }
  var isUnfilled = (np % numGroups) > 0;
  if (isUnfilled && adv >= grs - 1) {
    return "must advance less than the smallest match size";
  }
  if (adv <= 0) {
    return "must eliminate players each match";
  }
  return null;
};

var finalInvalid = function (leftOver, limit, gLast) {
  if (leftOver < 2) {
    return "must at least contain 2 players"; // force >4 when using limits
  }
  var lastNg = Math.ceil(leftOver / gLast);
  if (limit > 0) { // using limits
    if (limit >= leftOver) {
      return "limit must be less than the remaining number of players";
    }
    // need limit to be a multiple of numGroups (otherwise tiebreaks necessary)
    if (limit % lastNg !== 0) {
      return "number of groups must divide limit";
    }
  }
  else if (lastNg !== 1) {
    return "must contain a single match when not using limits";
  }
  return null;
};

var invalid = function (np, grs, adv, opts) {
  if (!Number.isFinite(np) || Math.ceil(np) !== np || np < 2) {
    return "number of players must be at least 2";
  }
  if (!Array.isArray(grs) || !Array.isArray(adv)) {
    return "adv and grs must be arrays";
  }
  if (!grs.length || !grs.every(Number.isFinite)) {
    return "grs must be a non-empty array of integers";
  }
  if (!adv.every(Number.isFinite) || grs.length !== adv.length + 1) {
    return "adv must be an array of integers of length grs.length - 1";
  }

  var numGroups = 0;
  for (var i = 0; i < adv.length; i += 1) {
    // loop over adv as then both a and g exist
    var a = adv[i];
    var g = grs[i];
    // calculate how big the groups are
    numGroups = Math.ceil(np / g);
    var gActual = algs.reduceGroupSize(np, g, numGroups);

    // and ensure with group reduction that eliminationValid for reduced params
    var invReason = roundInvalid(np, gActual, a, numGroups);
    if (invReason !== null) {
      return "round " + (i+1) + " " + invReason;
    }
    // return how many players left so that np is updated for next itr
    np = numGroups*a;
  }
  // last round and limit checks
  var invFinReason = finalInvalid(np, (opts && opts.limit) | 0, grs[grs.length-1]);
  if (invFinReason !== null) {
    return "final round: " + invFinReason;
  }

  // nothing found - ok to create
  return null;
};

var elimination = function (np, grs, adv, limit) {
  var invReason = invalid(np, grs, adv, {limit: limit});
  if (invReason !== null) {
    console.error("Invalid FFA configuration %dp sizes=%j, advs=%j, opts=%j"
      , np, grs, adv, {limit: limit});
    console.error("reason: %s", invReason);
    return {};
  }
  //console.log('creating %dp FFA elimination (%j/%j advancing)', np, adv, grs);

  var matches = []; // pushed in sort order
  // rounds created iteratively - know configuration valid at this point so just
  // repeat the calculation in the validation
  for (var i = 0; i < grs.length; i += 1) {
    var a = adv[i]
      , gs = grs[i]
      , numGroups = Math.ceil(np / gs)
      , gsActual = algs.reduceGroupSize(np, gs, numGroups);

    // irrelevant which gs we use as algs.groups reduces anyway
    // though might as well save it the effort and we need it here anyway
    var grps = algs.groups(np, gsActual);
    if (numGroups !== grps.length) {
      throw new Error("internal FFA construction error");
    }
    if (i > 0) {
      // only fill in seeding numbers for round 1, otherwise placeholders
      grps = unspecify(grps);
    }

    // fill in matches
    for (var m = 0; m < grps.length; m += 1) {
      matches.push({id: {s: 1, r: i+1, m: m + 1}, p: grps[m]}); // matches 1-indexed
    }
    // reduce players left (for next round - which will exist if a is defined)
    np = numGroups*a;
  }
  return matches;
};

// interface
function FFA(numPlayers, grs, advs, opts) {
  if (!(this instanceof FFA)) {
    return new FFA(numPlayers, grs, advs, opts);
  }
  this.version = 1;
  this.advs = advs;
  this.limit = opts ? opts.limit | 0 : 0;
  this.numPlayers = numPlayers;
  Base.call(this, elimination(numPlayers, grs, advs, this.limit));
}
FFA.prototype = Object.create(Base.prototype);
FFA.parse = Base.parse.bind(null, FFA);
FFA.invalid = invalid;
FFA.idString = idString;
FFA.prototype.rep = idString; // for now

FFA.prototype.unscorable = function (id, score, allowPast) {
  var invReason = Base.prototype.unscorable.call(this, id, score, allowPast);
  if (invReason !== null) {
    return invReason;
  }
  var adv = this.advs[id.r - 1] || 0;
  if (adv > 0 && score[adv] === score[adv - 1]) {
    return "scores must unambiguous decide who advances";
  }
  if (!adv && this.limit > 0) {
    // number of groups in last round is the match number of the very last match
    // because of the ordering this always works!
    var lastNG = this.matches[this.matches.length-1].id.m;
    var cutoff = this.limit/lastNG; // NB: lastNG divides limit (from finalInvalid)
    if (score[cutoff] === score[cutoff - 1]) {
      return "scores must decide who advances in final round with limits";
    }
  }
  return null;
};

var prepRound = function (currRnd, nxtRnd, adv) {
  var top = currRnd.map(function (m) {
    return $.zip(m.p, m.m).sort(Base.compareZip).slice(0, adv);
  });

  // now flatten and sort across matches
  // this essentially re-seeds players for the next round
  top = $.pluck(0, $.flatten(top).sort(Base.compareZip));

  // re-find group size from maximum length of zeroed player array in next round
  var grs = $.maximum($.pluck('length', $.pluck('p', nxtRnd)));

  // set all next round players with the fairly grouped set
  algs.groups(top.length, grs).forEach(function (group, k) {
    // replaced nulled out player array with seeds mapped to corr. top placers
    nxtRnd[k].p = group.map(function (seed) {
      return top[seed-1]; // NB: top is zero indexed
    });
  });
};

// updates ms in place and returns whether or not anything changed
FFA.prototype.score = function (id, score) {
  if (Base.prototype.score.call(this, id, score)) {
    var adv = this.advs[id.r - 1] || 0;
    var currRnd = this.findMatches({r: id.r});
    if (currRnd.every($.get('m')) && adv > 0) {
      prepRound(currRnd, this.findMatches({r: id.r + 1}), adv);
    }
    return true;
  }
  return false;
};

FFA.prototype.upcoming = function (playerId) {
  var id = Base.prototype.upcoming.call(this, playerId);
  if (id) {
    return id; // player not waiting for new rounds - match ready
  }

  // if player reached currentRound, he may be waiting for generation of nextRound
  var m = $.firstBy(function (m) {
    return m.p.indexOf(playerId) >= 0 && m.m;
  }, this.currentRound() || []);

  if (m) {
    // will he advance to nextRound ?
    var adv = this.advs[m.id.r - 1];
    if (Base.sorted(m).slice(0, adv).indexOf(playerId) >= 0) {
      return {s: 1, r: m.id.r + 1};
    }
  }
};

// helpers for results' round loop
var isReady = function (rnd) {
  return rnd.some(function (m) {
    return m.p.some($.neq(Base.NONE));
  });
};
var isDone = function (rnd) {
  return rnd.every($.get('m'));
};

FFA.prototype.results = function () {
  // TODO: best scores?
  var res = Base.prototype.results.call(this, { sum: 0 });
  var advs = this.advs;
  var maxround = 1;
  for (var i = 0; i < this.matches.length; i += 1) {
    var g = this.matches[i];
    maxround = Math.max(g.id.r, maxround);

    if (g.m) {
      // count stats for played matches
      var top = $.zip(g.p, g.m).sort(Base.compareZip);
      for (var j = 0; j < top.length; j += 1) {
        var pJ = top[j][0] - 1  // convert seed -> zero indexed player number
          , mJ = top[j][1];     // map wins for pJ

        var adv = advs[g.id.r - 1] || 0;
        // NB: final round win counted by .positionTies as can have multiple winners
        if (j < adv) {
          res[pJ].wins += 1;
        }
        res[pJ].sum += mJ;
      }
    }
  }

  var limit = this.limit;
  // gradually improve scores for each player by looking at later and later rounds
  this.rounds().forEach(function (rnd, k) {
    var adv = advs[k] || 0; // so we can do last round
    var rndPs = $.flatten($.pluck('p', rnd));

    if (isDone(rnd)) {
      if (limit > 0 && k === maxround - 1) {
        // this is the special case of a `limit`ed tournament
        // it may not be a 'final' in the sense that rnd.length >= 1
        // as the winnners goto a new tournament and losers are knocked out

        if (rndPs.length < limit) {
          // sanity check for own tests
          throw new Error("FFA internal error - too few players for forwarding");
        }
        // figure out how many to advance from each round
        // we know limit is a multiple of rnd.length
        if (limit % rnd.length !== 0) {
          throw new Error("FFA internal error - limit not multiple of maxrnd len");
        }
        adv = limit / rnd.length;

        // losers fall through and are scored as any other done round with 'adv' set
        // but we must positions winners as well (top adv each match) in this rnd
        // as nothing else does this in this special case
        rnd.forEach(function (m) {
          // loop through winners
          Base.sorted(m).slice(0, adv).forEach(function (w, i) {
            var resEl = res[w - 1];
            // no adv set for this round so must also increment wins for these
            resEl.wins += 1;
            // their final position shall be tied between groups, and desc within
            resEl.pos = i*rnd.length + 1;
          });
        });
      }
      // if round is done, position the ones not advancing from that round
      // collect and sort between round the ones after top adv
      // NB: in `limit`-less final, adv is zero, so everyone's treated as a loser
      rnd.forEach(function (m) {
        // start at: numPlayers - (numPlayers - adv*numGroups) + 1
        var start = rndPs.length - (rndPs.length - adv*rnd.length) + 1;

        // loop through losers
        Base.sorted(m).slice(adv).forEach(function (l, i) {
          var resEl = res[l - 1];
          if (i === 0 && adv === 0) {
            // set wins +1 on actual winner in `limit`-less final
            resEl.wins += 1;
          }
          // tie x-placing losers between groups (at least pos-wise)
          resEl.pos = start + i*rnd.length;
        });
      });
    }
    else if (isReady(rnd)) {
      // round is simply ready, make sure everyone who got here is tied
      rndPs.forEach(function (p) {
        // last place in round is simply number of players beneath
        res[p-1].pos = rndPs.length;
      });
    }
  });
  // still sort also by sum in case people want to use that
  return res.sort($.comparing('pos', +1, 'sum', -1));
};

module.exports = FFA;
<|endoftext|>