#rtm #tech

Remember The MilkのMilkScriptについて説明する

タスク管理ツールのRemember The MilkにはMikScriptなるスクリプト実行環境、およびエディタが付属している。何ができるかというと、そのエディタ内にデフォルトで組み込まれているライブラリを使って、JavaScriptでタスクのデータを操作できる。

例えばこういうことができる。

rtm というオブジェクトから getTasks()MilkScript が含まれるタスクを検索し、タスクオブジェクトの getName()getDueDate() でタスクの情報にアクセスできる。また実行環境でもあるため、上の Run からスクリプトを実行できる。

何が嬉しいの、というと、タスク管理ツールの機能を自分で作れる機能だと思っていて、例えば一連のタスクから報告用のタスクを作成する、それらを通知する、タスクの対応優先順位を動的に算出する、など、もともと汎用的なRemember The Milkのポテンシャルをさらに引き出す機能と考えている。

それくらい他のタスク管理ツールとは一線を画すめちゃくちゃ便利だと思っているんだけれど、世界には全然伝わっていないようなので、RTM応援団たる僕は、その便利さを分かっているだけここで述べることにする。そもそも分かっていることが少ない、公式からの情報もあまりない、第三者が共有しているナレッジもない、という三重苦のなかで書いている。

MilkScriptについて

MilkScriptの概要

MikScriptは2022年の9月、Proユーザーのみが使える機能として リリース された。今もProユーザーしか使えないので、すぐに会員登録してProユーザーになっていただくのも小粋だろう。

公式のドキュメントはこちらだ。

MilkScriptが使えるようになると、Remember The Milkの右上に下記のようなアイコンが表示されるはずだ。はずだ、とは書いているものの、無料ユーザーにはどのように表示されているかは分からない。とにかくProになるとこれが表示されるはずである。

僕の場合はすでにいくつかのMikScriptが登録されているので何となく一応、モザイクをかけてある。このように作成されたMikScriptはここに羅列される。 New... がスクリプトの新規作成、 Manage... を選ぶことで既存スクリプトの編集や削除ができるようになる。編集画面が下記の画像というわけだ。

GraalVM使用していると書かれている が、上のスクリプトが動くのを見るに、ES2022のようである。(Special Thanks:正式仕様リリース! JavaScriptの最新仕様ES2022で追加された「全」新機能

スクリプトの実行方法

作ったスクリプトの実行方法は3つ。

  • アプリケーションの右上からこのモザイクのかかっている部分をクリックする。
  • Remember The Milkと連携できるノーコードツールである IFTTTZapier と連携し、何らかのトリガーでMilkscriptを実行する。
  • Remember The MilkのAPI経由で実行する

まーーーーーー、APIは申請が必要で試したことがなく、まーーー使わないと思うので、今すぐ実行したいときはアプリケーションの画面から、定期的に実行したいときはIFTTTやZapierを使うといいのだろうと思う。

ちなみにMilkScriptのエディタ自体にGoogle App Scriptのような定期実行の機能はなく、もし定期実行をしたいのであれば、IFTTTかZapierを使え、と 公式も言っている

制限について

ここ でさっぱり語られているが、Milkscriptは無制限で実行できるわけではない。

MilkScript execution is limited to 200,000 statements or 10 seconds (whichever comes first). If you exceed the limitation, your script throws an exception and execution stops. All limits are subject to elimination, reduction, or change at any time, without notice.

ということで、20万ステートメントより多く実行されるか、実行時間10秒かのどちらか早く達した方がリミットのようだ。なにぶんタスクのデータを処理するという性質上、両方とも気をつけないと割と達することがある。処理するタスクの量を検索で制限するのがコツだと思う。リミットに達すると下記のような例外が投げられる。

MikScriptのサンプル

ここから先はよく使うMilkScriptのサンプルを書いておく。というか自分で書いたものだがJavaScriptには疎いので色々許されたい。指摘ください。というか、もうこのレベルのものはChatGPTに書いてもらうのが良さそう。

レポート用のタスクを作り、ノートにレポートを記載する

潤沢にテキストを書き込める場所がRemember The Milkにはノートしかないので、よくやる手法だと思う。何らかのテキストドキュメントを作り込み、タスクを一つ作り、そのノートに記載する。自分はこれをIFTTTで毎朝8時に実行するようにし、一日のタスクサマリとして活用している。(実際にはもっと把握しやすいようにタスク名をこねくり回している) 公式にも似たようなサンプルがある

MilkScriptはこちら
const today = new Date();
today.setHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);

const criteria = [
    "name:sample",
    "not tag:sample"
]

const getTaskReport = () => {
    let notes = [];
    const rtmTasks = rtm.getTasks(criteria.join(" AND "));

    // Due Dateでソート
    rtmTasks.sort((a, b) => {
       return a.getDueDate() - b.getDueDate();
    });

    let beforeToday = true;
    rtmTasks.forEach((task) => {

        // 明日以降のタスク
        if (beforeToday && task.getDueDate() >= tomorrow) {
            beforeToday = false;
            notes.push('^^^ Today');
        }

        notes.push('[] ' + task.getName());
    });

    return notes.join("\n");
}

let task = rtm.addTask('タスクレポート');

# 対象のリストのオブジェクトを取得し設定する
# この場合は action という名前のリストが取得される
const listAction = rtm.getLists().filter(l => l.getName() === 'action').pop();
task.setList(listAction);

task.addTags('sample');

const duDate = new Date();
duDate.setHours(8, 0, 0, 0);
task.setDueTime(duDate);

const estimate = rtm.newEstimate(0, 3);
task.setEstimate(estimate)

task.setPriority(rtm.Priority.Medium);

# レポートの内容を作ったタスクのノートに追加する
task.addNote(getTaskReport());

実行すると、下記のようなノートを持ったタスクが一つ出来上がる。

[] プロジェクト進捗状況更新
[] 予実確認
[] 電子申請の確認
^^^ Today
[] 提案資料の作成

タスクの対応優先順位を優先度と終了日から求める

Remember The Milkには優先度(Priority)を1〜4の4段階で定めることができるので、アイゼンハワーマトリクスを利用してタスクの優先順位を決めるようにしている。1が緊急で重要、2が緊急ではないが重要、3が緊急だが重要ではない、4が緊急でも重要でもない、と自分の中で決めている。ただ実際にはタスクの終了日も関わるので、3軸になってしまい、だいたいのタスク管理では表現が難しく、俯瞰的に眺める時間をとらなければならない。

このため、優先度と終了日をかけあわせて数値で表せる「タスクパワー」を算出し、タスク名に入れ込むようにした。情報が増えるので俯瞰性能は下がるが、タスク名のソート順を変えれば、タスクの優先順位を自動的に定めるのにまぁまぁの参考になる。

MilkScriptはこちら
// 今日の日付を取得
const NOW = new Date();

const calcTimeValue = (date) => {
    const hoursDiff = Math.abs(NOW - date) / 36e5;
    const scale = 10;

    let timeValue = 0;
    if(date > NOW) {
        timeValue = 50 / (hoursDiff / scale + 1);
    } else if (date < NOW) {
        timeValue = 50 + 50 * (hoursDiff / scale) / (1 + hoursDiff / scale);
    } else {
        timeValue = 50;
    }

    return timeValue;
}

const calcTaskPower = (task) => {

    let timeValue = null;
    let priorityWeight = null;

    // 終了日を取得
    const dueDate = task.getDueDate();

    // dueDateが入っていないことはリスクなので1000パワーで返してしまう
    if (!dueDate) {
        return 1000;
    }

    // 終了日が6時間以内で数分で終わらせられるものはtimeValueを高め(最小でも990)に設定する
    const estimate = task.getEstimate();
    if (estimate && estimate.getMinutes() <= 10) {
        const sixHours = 6 * 60 * 60 * 1000;
        if ((dueDate.getTime() - NOW.getTime() <= sixHours)) {
            timeValue = 100 - (1 - 1/estimate.getMinutes()) * 10;
            priorityWeight = 10.0;
        }
    }

    if (timeValue === null) {
        timeValue = calcTimeValue(task.getDueDate())
    }

    if (priorityWeight === null) {
        switch(String(task.getPriority())) {
            case String(rtm.Priority.High):
                priorityWeight = 10.0;
                break;
            case String(rtm.Priority.Medium):
                priorityWeight = 7.0;
                break;
            case String(rtm.Priority.Low):
                priorityWeight = 4.0;
                break;
            default:
                priorityWeight = 1.0;
                break;
        }
    }

    return timeValue * priorityWeight;
}

const main = () => {
    let tasks = rtm.getTasks('list:action AND status:incomplete');

    tasks.forEach((task) => {
        const taskPower = calcTaskPower(task);

        const tp = 'TP.' + String(Math.ceil(taskPower)).padStart(4, '0');
        const orgName = task.getName().replace(/^TP\.\d*?/, '');

        task.setName(tp + '' + orgName);
    });
}

main();

仕組みは現在時刻から終了日が近ければ近いほど高くなるtimeValueとPriorityをかけて最大1000の値をタスクパワーとして算出している。をこれを実行すると action というリストに含まれる未完了のタスク全ての頭に TP.xxxx というような数値が入る。

画像のものはどうでもいいタスクばかりなのでタスクパワーは低いけれども。すぐ終わるタスクなどは990などが設定されて、すぐやれ、っていうソート順になる。優先度が低ければ低いほど、終了日が遠ければ遠いほどタスクパワーは低くなる。

これもやはりIFTTTで1時間ごとに実行しており、タスクの優先度を動的に変更している。ちなみにこんなこともできるんだよ、的なもので、自分も現状、テスト的に実施しているタスク管理方法なので、この運用は全くおすすめしない。タスク名変えちゃうし。

いかがでしたでしょうか

MilkScriptはシンプルにJSが動くので、やろうと思えばアイディア次第で様々なタスク管理が実現できる。Google DriveにデータをPOSTする、Slackに通知するなどもできる。ただ、残念ながらGithub Actionsの secrets みたいなのが提供されていないので、秘匿情報を埋め込むのはおすすめはしない。

あとrequireとかができず、jsのパッケージを読み込むことができないかもしれない。この辺はよく分かってない。その辺を読み込んでいたらあっというまに20万ステートメントのリミットの枠には達しそうな気もするが。サンプルも含めて、もっと分かってきたら、この記事を更新したい。

ふふふ、Remember The Milk、かわいいやつ。


合わせて読みたい