MilkScript

Automate tasks with MilkScript.

menu

Quickstart: Monthly stats

The below quickstart sample creates a script that calculates the stats of your tasks completed or added in the last month.

When you run the script, it will add Monthly Stats: October 2024 task with the stats in the notes to your default list.

Set it up

  1. Open the web app or desktop app.
  2. Click the MilkScript button at the top right, then click New...
  3. Copy the code below and paste it into the script editor.
const now = new Date();

const thisMonth = new Date(now.getFullYear(), now.getMonth());
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1);
const penultimateMonth = new Date(now.getFullYear(), now.getMonth() - 2);

const completedLastMonth = searchTasks('completed', lastMonth, thisMonth);
const completedPenultimateMonth =
  searchTasks('completed', penultimateMonth, lastMonth);

const addedLastMonth = searchTasks('added', lastMonth, thisMonth);
const addedPenultimateMonth =
  searchTasks('added', penultimateMonth, lastMonth);

const taskName =
  `Monthly Stats: ${formatMonth(lastMonth)} ${lastMonth.getFullYear()}`;

const addedStats = formatStats(
  'Tasks added', addedLastMonth, addedPenultimateMonth);

const completedStats = formatStats(
  'Tasks completed', completedLastMonth, completedPenultimateMonth);

rtm.addTask(taskName)
  .addNote(addedStats)
  .addNote(completedStats);

/**
 * Formats the given date as a month:
 *
 * 2022-01-25T06:46:08.623Z ===> January
 *
 * 2021-12-01T08:00:00.123Z ===> December
 *
 * @return {string}
 * @param {!Date} date
 */
function formatMonth(date) {
  return date.toLocaleString('default', { month: 'long' });
}

/**
 * Formats the percentage difference between the given values:
 *
 * newValue = 10, oldValue = 5 ===> (↑100.00%)
 *
 * newValue = 7, oldValue = 33 ===> (↓78.79%)
 *
 * @return {string}
 * @param {number} newValue
 * @param {number} oldValue
 */
function formatRate(newValue, oldValue) {
  if (newValue === oldValue || newValue === 0 || oldValue === 0) {
    return '';
  }

  const rate = -(100 - newValue / oldValue * 100);

  return ` (${rate < 0 ? '↓' : '↑'}${Math.abs(rate).toFixed(2)}%)`;
}

/**
 * Formats the estimate section of the stats:
 *
 * name = "Total estimate", estimate = rtm.newEstimate(2, 35) ===>
 *
 *     TOTAL ESTIMATE
 *     2 hours 35 minutes
 *
 * @return {!Array<string>}
 * @param {!rtm.Estimate} estimate
 */
function formatEstimate(name, estimate) {
  if (estimate.getMinutes() === 0) {
    return [];
  }

  const hours = Math.trunc(estimate.getMinutes() / 60);
  const minutes = estimate.getMinutes() % 60;

  return [
    '',
    name.toUpperCase(),
    [
      hours === 1 ? '1 hour' : `${hours} hours`,
      minutes === 1 ? '1 minute' : `${minutes} minutes`
    ].join(' ')
  ];
}

/**
 * Groups the given items and formats them as a list:
 *
 * name = "By list", items = ["Inbox", "Inbox", "Work", "Inbox"] ===>
 *
 *     BY LIST
 *       • Inbox: 3
 *       • Work: 1
 *
 * @return {!Array<string>}
 * @param {string} name
 * @param {!Array<string>} items
 */
function formatCounts(name, items) {
  if (items.length === 0) {
    return '';
  }

  const counts = items.reduce((result, item) =>
    result.set(item, (result.get(item) ?? 0) + 1), new Map());

  return [
    '',
    name.toUpperCase(),
    ...Array.from(counts)
        .sort(([item1, count1], [item2, count2]) =>
          count2 - count1 || item2.localeCompare(item1))
        .map(([item, count]) => `  • ${item}: ${count}`)
  ];
}

/**
 * Formats the total section of the stats:
 *
 * name = "Tasks completed", count = 12, previousCount = 50 ===>
 *
 *     TASKS COMPLETED
 *     12 (↓76.00%)
 *
 * @return {!Array<string>}
 * @param {string} name
 * @param {number} count
 * @param {number} previousCount
 */
function formatTotal(name, count, previousCount) {
  return [
    name.toUpperCase(),
    `${count}${formatRate(count, previousCount)}`
  ];
}

/**
 * @return {string}
 * @param {string} statsName
 * @param {!Array<!rtm.Task>} tasks — Last month's tasks.
 * @param {!Array<!rtm.Task>} previousTasks — Penultimate month's tasks.
 */
function formatStats(statsName, tasks, previousTasks) {
  const zeroEstimate = rtm.newEstimate(0);

  const totalEstimate = tasks.reduce((result, task) =>
    result.plus(task.getEstimate() ?? zeroEstimate), zeroEstimate);

  const lists = tasks.map(task => task.getList().getName());

  const tags = tasks
    .flatMap(task => task.getTags())
    .map(tag => tag.getName());

  const locations = tasks.map(task =>
    task.getLocation()?.getName() ?? 'No Location');

  return [
    ...formatTotal(statsName, tasks.length, previousTasks.length),
    ...formatEstimate('Total estimate', totalEstimate),
    ...formatCounts('By list', lists),
    ...formatCounts('By tag', tags),
    ...formatCounts('By location', locations)
  ].join('\n');
}

/**
 * Searches tasks within the given date range.
 *
 * @param {string} queryPrefix — Either 'completed' or 'added'.
 * @param {!Date} begin
 * @param {!Date} end
 */
function searchTasks(queryPrefix, begin, end) {
  const formatDate =
    date => `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;

  const query = [
    `${queryPrefix}After:${formatDate(begin)}`,
    `${queryPrefix}Before:${formatDate(end)}`
  ].join(' AND ');

  return rtm.getTasks(query);
}
  1. At the top left, click Untitled script.
  2. Enter a name for your script (e.g. Create Monthly Stats), then close the script editor.

Try it out

  1. Click the MilkScript button at the top right.
  2. Click the recently created script, then click Yes, run script.
  3. When the script execution completes, check your default list for a new Monthly Stats: October 2024 task with the stats in the notes.