leetcode-cli
Version:
A cli tool to enjoy leetcode!
226 lines (192 loc) • 6.48 kB
JavaScript
'use strict';
var moment = require('moment');
var _ = require('underscore');
var chalk = require('../chalk');
var icon = require('../icon');
var log = require('../log');
var core = require('../core');
var session = require('../session');
var sprintf = require('../sprintf');
var h = require('../helper');
const cmd = {
command: 'stat',
desc: 'Show statistics',
aliases: ['stats', 'progress', 'report'],
builder: function(yargs) {
return yargs
.option('c', {
alias: 'cal',
type: 'boolean',
default: false,
describe: 'Show calendar statistics'
})
.option('g', {
alias: 'graph',
type: 'boolean',
default: false,
describe: 'Show graphic statistics'
})
.option('l', {
alias: 'lock',
type: 'boolean',
default: true,
describe: 'Include locked questions'
})
.option('q', core.filters.query)
.option('t', core.filters.tag)
.example(chalk.yellow('leetcode stat'), 'Show progress status')
.example(chalk.yellow('leetcode stat -g'), 'Show detailed status in graph')
.example(chalk.yellow('leetcode stat -c'), 'Show accepted status in calendar')
.example('', '')
.example(chalk.yellow('leetcode stat --no-lock'), 'Show status without locked questions')
.example(chalk.yellow('leetcode stat -t algorithms'), 'Show status of algorithms questions only')
.example(chalk.yellow('leetcode stat -q h'), 'Show status of hard questions only');
}
};
function printLine(key, done, all) {
const n = 30;
const percent = (all > 0) ? done / all : 0;
const x = Math.ceil(n * percent);
log.printf(' %s\t%3s/%-3s (%6s %%) %s%s',
h.prettyLevel(key), done, all,
(100 * percent).toFixed(2),
chalk.green('█'.repeat(x)),
chalk.red('░'.repeat(n - x)));
}
function showProgress(problems) {
const stats = {
easy: {all: 0, ac: 0},
medium: {all: 0, ac: 0},
hard: {all: 0, ac: 0}
};
for (let problem of problems) {
const level = problem.level.toLowerCase();
const state = problem.state.toLowerCase();
if (!(level in stats)) continue;
++stats[level].all;
if (!(state in stats[level])) continue;
++stats[level][state];
}
printLine('Easy', stats.easy.ac, stats.easy.all);
printLine('Medium', stats.medium.ac, stats.medium.all);
printLine('Hard', stats.hard.ac, stats.hard.all);
}
function showGraph(problems) {
const ICONS = {
ac: chalk.green(icon.ac),
notac: chalk.red(icon.notac),
none: chalk.gray(icon.none),
empty: icon.empty
};
// row header is 4 bytes
// each question takes 2 bytes
// each group has 10 questions, which takes (2*10=20) + 3 paddings
let groups = Math.floor((h.width - 4) / (3 + 2 * 10));
if (groups < 1) groups = 1;
if (groups > 5) groups = 5;
const header = _.range(groups)
.map(x => sprintf('%4s%18s', x * 10 + 1, x * 10 + 10))
.join('');
log.info(' ' + header);
const graph = [];
for (let problem of problems)
graph[problem.fid] = ICONS[problem.state] || ICONS.none;
let rowNumFormat = ' %04s';
let line = [sprintf(rowNumFormat, 0)];
for (let i = 1, n = graph.length; i <= n; ++i) {
// padding before group
if (i % 10 === 1) line.push(' ');
line.push(graph[i] || ICONS.empty);
// time to start new row
if (i % (10 * groups) === 0 || i === n) {
log.info(line.join(' '));
line = [sprintf(rowNumFormat, i)];
}
}
log.info();
log.printf('%7s%s%3s%s%3s%s',
' ', ICONS.ac + chalk.green(' Accepted'),
' ', ICONS.notac + chalk.red(' Not Accepted'),
' ', ICONS.none + ' Remaining');
}
function showCal(problems) {
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const ICONS = [
icon.none,
chalk.white(icon.ac),
chalk.green(icon.ac),
chalk.yellow(icon.ac),
chalk.red(icon.ac)
];
const N_MONTHS = 12;
const N_WEEKS = 53;
const N_WEEKDAYS = 7;
const now = moment();
const SCORES = {easy: 1, medium: 2, hard: 5};
function toScore(sum, id) {
const problem = problems.find(x => x.fid === id);
if (problem) sum += (SCORES[problem.level.toLowerCase()] || 1);
return sum;
}
// load historical stats
const graph = [];
const stats = require('../cache').get(h.KEYS.stat) || {};
for (let k of _.keys(stats)) {
const score = (stats[k]['ac.set'] || []).reduce(toScore, 0);
if (score === 0) continue;
const d = moment(k, 'YYYY-MM-DD');
graph[now.diff(d, 'days')] = score;
}
// print header
const buf = Buffer.alloc(120, ' ', 'ascii');
for (let i = 0; i <= N_MONTHS; ++i) {
// for day 1 in each month, calculate its column position in graph
const d = now.clone().subtract(i, 'months').date(1);
const idx = now.diff(d, 'days');
const j = (N_WEEKS - idx / N_WEEKDAYS + 1) * 2;
if (j >= 0) buf.write(MONTHS[d.month()], j);
}
log.printf('%7s%s', ' ', buf.toString());
// print graph
for (let i = 0; i < N_WEEKDAYS; ++i) {
const line = [];
// print day in week
const idx = (now.day() + i + 1) % N_WEEKDAYS;
line.push(sprintf('%4s ', WEEKDAYS[idx]));
for (let j = 0; j < N_WEEKS; ++j) {
let idx = (N_WEEKS - j - 1) * N_WEEKDAYS + N_WEEKDAYS - i - 1;
const d = now.clone().subtract(idx, 'days');
// map count to icons index:
// [0] => 0, [1,5] => 1, [6,10] => 2, [11,15] => 3, [16,) => 4
const count = graph[idx] || 0;
idx = Math.floor((count - 1) / 5) + 1;
if (idx > 4) idx = 4;
let icon = ICONS[idx];
// use different colors for adjacent months
if (idx === 0 && d.month() % 2) icon = chalk.gray(icon);
line.push(icon);
}
log.info(line.join(' '));
}
log.info();
log.printf('%8s%s%3s%s%3s%s%3s%s',
' ', ICONS[1] + ' 1~5',
' ', ICONS[2] + ' 6~10',
' ', ICONS[3] + ' 11~15',
' ', ICONS[4] + ' 16+');
}
cmd.handler = function(argv) {
session.argv = argv;
core.filterProblems(argv, function(e, problems) {
if (e) return log.fail(e);
if (!argv.lock)
problems = problems.filter(x => !x.locked);
log.info();
if (argv.graph) showGraph(problems);
else if (argv.cal) showCal(problems);
else showProgress(problems);
log.info();
});
};
module.exports = cmd;