Skip to content

Instantly share code, notes, and snippets.

@hangingman
Created January 29, 2025 03:49
Show Gist options
  • Save hangingman/1f9c29c77c0b1544b02d9bfeb662704d to your computer and use it in GitHub Desktop.
Save hangingman/1f9c29c77c0b1544b02d9bfeb662704d to your computer and use it in GitHub Desktop.
todo.txtのpegでのパース設計

以下に、これまでの実装内容をまとめてテキスト化したものを記載します。


1. 型定義 (Todo インターフェース)

export interface Todo {
  text: string;       // タスクの本文
  date: string;       // 作成日 (YYYY-MM-DD)
  id: string;         // タスクID (id:xxx 形式から抽出)
  completed: boolean; // 完了状態
  projects: string[]; // プロジェクト (+で始まる要素の配列)
  contexts: string[]; // コンテキスト (@で始まる要素の配列)
}

2. PEG文法ファイル (todo-grammar.pegts)

{
  interface TodoPart {
    completed: boolean;
    date: string;
    id: string;
    text: string;
    projects: string[];
    contexts: string[];
  }

  function processRest(rest: string): { id: string, text: string, projects: string[], contexts: string[] } {
    const idMatch = rest.match(/(?:^|\s)id:([^\s]+)/);
    const id = idMatch?.[1] || '';
    const projects = [...rest.matchAll(/\+(\S+)/g)].map(m => m[1]);
    const contexts = [...rest.matchAll(/@(\S+)/g)].map(m => m[1]);
    const text = rest
      .replace(/(?:^|\s)id:[^\s]+/g, '')
      .replace(/[+@]\S+/g, '')
      .replace(/\s+/g, ' ')
      .trim();
    return { id, text, projects, contexts };
  }
}

TodoLine
  = _? line:(
      CompletedPart
      / UncompletedPart
    ) _? {
      return {
        completed: line.completed,
        date: line.date,
        id: line.id,
        text: line.text,
        projects: line.projects,
        contexts: line.contexts
      };
    }

CompletedPart
  = "x" __ completionDate:Date? __ createdDate:Date? __ rest:RestOfLine {
    return {
      completed: true,
      date: createdDate || "",
      ...processRest(rest)
    };
  }

UncompletedPart
  = priority:Priority? __ createdDate:Date? __ rest:RestOfLine {
    const priorityText = priority ? `(${priority}) ` : '';
    return {
      completed: false,
      date: createdDate || "",
      ...processRest(priorityText + rest)
    };
  }

Priority
  = "(" $[A-Z] ")" { return text().substring(1, 2); }

Date
  = date:$[0-9]{4}-[0-9]{2}-[0-9]{2} { return date; }

RestOfLine
  = rest:.* { return rest.join('').trim(); }

__ = [ \t]+
_ = [ \t]*

3. パーサー生成とメイン関数

import { parse } from './todo-grammar'; // tspegで生成されたパーサー

export function parseTodoLine(line: string): Todo {
  try {
    const parsed = parse(line);
    return {
      completed: parsed.completed,
      date: parsed.date,
      id: parsed.id,
      text: parsed.text,
      projects: parsed.projects,
      contexts: parsed.contexts
    };
  } catch (e) {
    return {
      completed: false,
      date: '',
      id: '',
      text: line.trim(),
      projects: [],
      contexts: []
    };
  }
}

4. 使用例

const line1 = "x 2023-10-01 2022-09-15 Finish project +work +urgent @office id:PROJ-123";
const todo1 = parseTodoLine(line1);
/* 結果:
{
  completed: true,
  date: "2022-09-15",
  id: "PROJ-123",
  text: "Finish project",
  projects: ["work", "urgent"],
  contexts: ["office"]
}
*/

const line2 = "(B) 2023-01-01 Start new year resolution +personal @home id:NY2023";
const todo2 = parseTodoLine(line2);
/* 結果:
{
  completed: false,
  date: "2023-01-01",
  id: "NY2023",
  text: "(B) Start new year resolution",
  projects: ["personal"],
  contexts: ["home"]
}
*/

5. 主な特徴

  1. 複数のプロジェクトとコンテキストを抽出

    • +project@context を配列として保持
    • 例: +work +urgentprojects: ["work", "urgent"]
  2. 完了タスクの処理

    • x で始まるタスクを完了済みと判定
    • 完了日と作成日を区別(作成日をdateに格納)
  3. 優先度処理

    • (A) 形式の優先度を検出しテキストに保持
  4. ID抽出

    • id:xxx 形式のIDを抽出して専用フィールドに格納
  5. エラーハンドリング

    • パース失敗時は入力をそのままテキストに格納

6. テストケース例

const line3 = "2023-12-25 +shopping @gift プレゼントを買う id:XMAS2023";
const todo3 = parseTodoLine(line3);
/* 結果:
{
  completed: false,
  date: "2023-12-25",
  id: "XMAS2023",
  text: "プレゼントを買う",
  projects: ["shopping"],
  contexts: ["gift"]
}
*/

const line4 = "x 2023-11-11 2023-10-10 +work @meeting 会議の議事録を書く id:MEET-456";
const todo4 = parseTodoLine(line4);
/* 結果:
{
  completed: true,
  date: "2023-10-10",
  id: "MEET-456",
  text: "会議の議事録を書く",
  projects: ["work"],
  contexts: ["meeting"]
}
*/

この実装により、todo.txtの仕様に従って1行をパースし、プロジェクトやコンテキストを含むすべての情報を適切に抽出できます。必要に応じて、さらに拡張や調整が可能です! 🚀

@hangingman
Copy link
Author

1. 型定義の修正(priorityフィールド追加)

export interface Todo {
  text: string;
  date: string;
  id: string;
  completed: boolean;
  priority: string;    // 追加 (A-Z or 空文字列)
  projects: string[];
  contexts: string[];
}

2. PEG文法ファイルの修正

{
  interface TodoPart {
    completed: boolean;
    date: string;
    id: string;
    text: string;
    priority: string;  // 追加
    projects: string[];
    contexts: string[];
  }

  function processRest(
    rest: string,
    priority?: string  // 優先度を引数で受け取る
  ): { 
    id: string, 
    text: string, 
    priority: string,
    projects: string[], 
    contexts: string[] 
  } {
    const idMatch = rest.match(/(?:^|\s)id:([^\s]+)/);
    const id = idMatch?.[1] || '';
    
    // 優先度記号をテキストから削除
    const cleanedRest = priority 
      ? rest.replace(`(${priority}) `, '')
      : rest;

    // プロジェクト/コンテキスト抽出
    const projects = [...cleanedRest.matchAll(/\+(\S+)/g)].map(m => m[1]);
    const contexts = [...cleanedRest.matchAll(/@(\S+)/g)].map(m => m[1]);

    // 最終テキストのクリーニング
    const text = cleanedRest
      .replace(/(?:^|\s)id:[^\s]+/g, '')
      .replace(/[+@]\S+/g, '')
      .replace(/\s+/g, ' ')
      .trim();

    return { 
      id, 
      text,
      priority: priority || '', 
      projects, 
      contexts 
    };
  }
}

// CompletedPartの修正
CompletedPart
  = "x" __ completionDate:Date? __ createdDate:Date? __ rest:RestOfLine {
    return {
      completed: true,
      date: createdDate || "",
      ...processRest(rest)
    };
  }

// UncompletedPartの修正
UncompletedPart
  = priority:Priority? __ createdDate:Date? __ rest:RestOfLine {
    return {
      completed: false,
      date: createdDate || "",
      ...processRest(rest, priority)  // 優先度を渡す
    };
  }

// Priorityルールの改良(空文字列許容)
Priority
  = "(" $[A-Z] ")" { return text().substring(1, 2); }
  / "" { return ""; }  // 優先度なしの場合

3. パーサー関数の修正

export function parseTodoLine(line: string): Todo {
  try {
    const parsed = parse(line);
    return {
      completed: parsed.completed,
      date: parsed.date,
      id: parsed.id,
      priority: parsed.priority,  // 追加
      text: parsed.text,
      projects: parsed.projects,
      contexts: parsed.contexts
    };
  } catch (e) {
    return {
      completed: false,
      date: '',
      id: '',
      priority: '',
      text: line.trim(),
      projects: [],
      contexts: []
    };
  }
}

4. テストケース例

const line = "(A) 2023-10-01 +work @office レポートを書く id:123";
const parsed = parseTodoLine(line);

/* 結果:
{
  completed: false,
  date: "2023-10-01",
  id: "123",
  priority: "A",          // 優先度が追加
  text: "レポートを書く",  // 優先度記号が除去された
  projects: ["work"],
  contexts: ["office"]
}
*/

主な修正点

  1. 優先度の分離処理

    • (A) 形式をテキストから除去してpriorityフィールドに格納
    • 例: "(A) タスク" → priority: "A", text: "タスク"
  2. 優先度のないケースの対応

    • 優先度がない場合は空文字列を設定
  3. テキストのクリーンアップ

    • 優先度記号がテキストに残らないように修正
    • プロジェクト/コンテキスト記号も除去
  4. 型定義の整合性

    • Todoインターフェースにpriorityフィールドを追加

これで優先度が正しく処理されるようになりました! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment