見出し画像

【Python】バレットジャーナルをDynalistで実現する


どんなバレットジャーナルを作ったか

カレンダーやタスクリストなどは、「何が使いやすいと感じるか」に個人差が大きい。以前は、私は予定日の決まっているものと繰り返しのあるものはGoogleカレンダー、日々のタスクリストはB6判の横罫ノートに手書きで書いておき、終わったら消すという方法をとっていた。

その後、クアデルノ用に富士通が配っているスケジューラや、YouTubeチャンネル「My Deep Guide」の運営者Vojaの販売している"MDO"(My Deep Guide Organizer)を使ってみたが、日々の予定ページは使うものの、週間予定や月間予定など、それ以上のものはどうしても馴染まなかった。

そこで試してみたのが、バレットジャーナルみたいなものをアウトラインプロセッサーの「Dynalist」で実現したもの。こんなのだ。

7/29から8/13までの予定が入っている。その月にやるべきこと、週ごとにやるべきこと、日ごとにやるべきことをリストで入力できる。インデントで字下げしている部分は、まとめて隠せる

さて、ここで問題になるのが、この雛形を作る手間だ。これを全部タイプしていたら、途中で嫌になってくる。そこでPythonを使ってみよう。

なお、コードを見たい人は読み飛ばして、最後のところにジャンプしてほしい。コード生成には、「Claude 3.5 Sonnet」を利用した。

とりあえずExcel VBAでやってみる

自分では最初から最後までコードを書くことはできない。そこで、今回は日付から文字列を作るところまで、まずExcel VBAでオリジナル関数を作って「29/Jul 2024 Mo」などの文字列を簡単に出力できるようにした。VBA無しでも関数を駆使すればなんとかなるが、IF関数の入れ子が鬱陶しくて、むしろ面倒。

英語ならオリジナル関数は不要だが、今回はドイツ語を使ってみたかったので、自作関数を作った。上のMONTHD関数は、数値で与えられた月を略称に変換する。下のWEEKDAYD関数は、日付から出した曜日を略称に変換する。いずれも、英語とドイツ語で微妙に異なるので、自作関数を用意した。英語でよければ不要だ
A列に日付、B列に日、C列にMONTHD関数で月の略称、D列にWEEKDAYD関数で曜日の略称を求めた。そして、E列でCONCAT関数で連結している

作成できた項目をPythonで処理する

E列の値をコピーしてAIに与え、OPML形式に変換して、Dynalistで読み込めばよい。実際には、E列には数式が入っているので、F列に値だけコピーしてそちらをコピーする。

どんなOPML形式に変換すればよいかは、自分でDynalistからの出力を見て、AIに指示を与えた。

で、できたものがこれ。

import re
from datetime import datetime, timedelta
import pyperclip
import xml.etree.ElementTree as ET

def process_date_list(date_list):
    grouped_dates = []
    current_group = []
    current_year = ""
    
    for date_str in date_list:
        date, day = date_str.rsplit(' ', 1)
        current_group.append(date_str)
        
        if day == 'So':  # End of the week
            start_date = current_group[0].split(' ')[0]
            end_date, year = current_group[-1].rsplit(' ', 1)
            if year != current_year:
                weekly_header = f"{start_date} - {end_date} {year} wöchentlich"
                daily_header = f"{start_date} - {end_date} {year} täglich"
                current_year = year
            else:
                weekly_header = f"{start_date} - {end_date} wöchentlich"
                daily_header = f"{start_date} - {end_date} täglich"
            grouped_dates.append(('weekly', weekly_header, daily_header, current_group))
            current_group = []
    
    # Handle the last group if it doesn't end with 'So'
    if current_group:
        start_date = current_group[0].split(' ')[0]
        end_date, year = current_group[-1].rsplit(' ', 1)
        if year != current_year:
            weekly_header = f"{start_date} - {end_date} {year} wöchentlich"
            daily_header = f"{start_date} - {end_date} {year} täglich"
        else:
            weekly_header = f"{start_date} - {end_date} wöchentlich"
            daily_header = f"{start_date} - {end_date} täglich"
        grouped_dates.append(('weekly', weekly_header, daily_header, current_group))
    
    return grouped_dates

def create_opml(grouped_dates):
    root = ET.Element("opml", version="1.0")
    head = ET.SubElement(root, "head")
    ET.SubElement(head, "title").text = "Grouped Dates"
    body = ET.SubElement(root, "body")

    for _, weekly_header, daily_header, dates in grouped_dates:
        weekly_outline = ET.SubElement(body, "outline", text=weekly_header, _note="", heading="2", colorLabel="3")
        daily_outline = ET.SubElement(body, "outline", text=daily_header, _note="", heading="2", colorLabel="5")
        for date in dates:
            ET.SubElement(daily_outline, "outline", text=date, _note="", heading="3")

    return ET.tostring(root, encoding="unicode", method="xml")

# Input date list
date_list = [
    "5/Aug 2024 Mo", "6/Aug 2024 Di", "7/Aug 2024 Mi", "8/Aug 2024 Do", "9/Aug 2024 Fr",
    "10/Aug 2024 Sa", "11/Aug 2024 So", "12/Aug 2024 Mo", "13/Aug 2024 Di", "14/Aug 2024 Mi",
    "15/Aug 2024 Do", "16/Aug 2024 Fr", "17/Aug 2024 Sa", "18/Aug 2024 So", "19/Aug 2024 Mo",
    "20/Aug 2024 Di", "21/Aug 2024 Mi", "22/Aug 2024 Do", "23/Aug 2024 Fr", "24/Aug 2024 Sa",
    "25/Aug 2024 So", "26/Aug 2024 Mo", "27/Aug 2024 Di", "28/Aug 2024 Mi", "29/Aug 2024 Do",
    "30/Aug 2024 Fr", "31/Aug 2024 Sa", "1/Sep 2024 So", "2/Sep 2024 Mo", "3/Sep 2024 Di",
    "4/Sep 2024 Mi", "5/Sep 2024 Do", "6/Sep 2024 Fr", "7/Sep 2024 Sa", "8/Sep 2024 So"
]

# Process the date list
result = process_date_list(date_list)

# Create OPML
opml_output = create_opml(result)

# Copy the result to clipboard
pyperclip.copy(opml_output)

print("OPML形式の結果がクリップボードにコピーされました。")

このコードを実行すると、出力がクリップボードにコピーされるので、テキストファイルに貼り付けて、拡張子を「.opml」とすればいい。あとは、Dynalistで読み込むだけ。

定数が目障りなので修正

ただ、date_listにゾロゾロと定数が並んでいて、どうも不全感がある。このままでは、次に更新する際、Excelでdate_listを作って、PythonでOPMLを作成し…となってしまう。ということで、まず日付を入力したらdate_listを出力するプログラムを作成。前項で作成したプログラムと連結させた。

実行すると「開始日:」で止まるので、「2024/8/5」と入力。次は「終了日:」に「2024/11/3」と入力すれば、OPMLがクリップボードにコピーされる

さて、これでOPML形式のテキストができたので、Dynalistに読み込んでみたところ、ちゃんと動作した。

出力先を新規OPMLファイルに変更

あとは、OPMLのテキストをコピーするのは面倒なので、ファイルを「./Downloads」フォルダーに作ってもらうようにさらに修正。

最終的にできたのが以下のコード。

rom datetime import datetime, timedelta
import xml.etree.ElementTree as ET
import os
import random

def get_date_input(prompt):
    while True:
        date_str = input(prompt)
        try:
            return datetime.strptime(date_str, "%Y/%m/%d")
        except ValueError:
            print("無効な日付形式です。YYYY/MM/DD の形式で入力してください。")

def german_month_abbr(month):
    german_months = ['Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun', 
                     'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
    return german_months[month - 1]

def generate_date_list(start_date, end_date):
    date_list = []
    current_date = start_date
    weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']

    while current_date <= end_date:
        date_str = f"{current_date.day}/{german_month_abbr(current_date.month)} {current_date.year} {weekdays[current_date.weekday()]}"
        date_list.append(date_str)
        current_date += timedelta(days=1)

    return date_list

def process_date_list(date_list):
    grouped_dates = []
    current_group = []
    current_year = ""
    
    for date_str in date_list:
        date, day = date_str.rsplit(' ', 1)
        current_group.append(date_str)
        
        if day == 'So':  # End of the week
            start_date = current_group[0].split(' ')[0]
            end_date, year = current_group[-1].rsplit(' ', 1)
            if year != current_year:
                weekly_header = f"{start_date} - {end_date} {year} wöchentlich"
                daily_header = f"{start_date} - {end_date} {year} täglich"
                current_year = year
            else:
                weekly_header = f"{start_date} - {end_date} wöchentlich"
                daily_header = f"{start_date} - {end_date} täglich"
            grouped_dates.append(('weekly', weekly_header, daily_header, current_group))
            current_group = []
    
    # Handle the last group if it doesn't end with 'So'
    if current_group:
        start_date = current_group[0].split(' ')[0]
        end_date, year = current_group[-1].rsplit(' ', 1)
        if year != current_year:
            weekly_header = f"{start_date} - {end_date} {year} wöchentlich"
            daily_header = f"{start_date} - {end_date} {year} täglich"
        else:
            weekly_header = f"{start_date} - {end_date} wöchentlich"
            daily_header = f"{start_date} - {end_date} täglich"
        grouped_dates.append(('weekly', weekly_header, daily_header, current_group))
    
    return grouped_dates

def create_opml(grouped_dates):
    root = ET.Element("opml", version="1.0")
    head = ET.SubElement(root, "head")
    ET.SubElement(head, "title").text = "Grouped Dates"
    body = ET.SubElement(root, "body")

    for _, weekly_header, daily_header, dates in grouped_dates:
        weekly_outline = ET.SubElement(body, "outline", text=weekly_header, _note="", heading="2", colorLabel="3")
        daily_outline = ET.SubElement(body, "outline", text=daily_header, _note="", heading="2", colorLabel="5")
        for date in dates:
            ET.SubElement(daily_outline, "outline", text=date, _note="", heading="3")

    return ET.tostring(root, encoding="unicode", method="xml")

def generate_filename():
    now = datetime.now()
    random_number = random.randint(100, 999)
    return f"{now.strftime('%Y%m%d_%H%M%S')}_{random_number}.opml"

def save_to_file(content):
    downloads_folder = os.path.expanduser("~/Downloads")
    filename = generate_filename()
    full_path = os.path.join(downloads_folder, filename)
    
    with open(full_path, 'w', encoding='utf-8') as f:
        f.write(content)
    
    return full_path

def main():
    print("日付範囲を指定してください(形式: YYYY/MM/DD)")
    start_date = get_date_input("開始日: ")
    end_date = get_date_input("終了日: ")

    if start_date > end_date:
        print("エラー: 開始日は終了日より前である必要があります。")
        return

    date_list = generate_date_list(start_date, end_date)
    
    # Process the date list
    result = process_date_list(date_list)

    # Create OPML
    opml_output = create_opml(result)

    # Save to file
    saved_path = save_to_file(opml_output)

    print(f"OPML形式の結果が以下のファイルに保存されました: {saved_path}")

if __name__ == "__main__":
    main()

これで問題なくプログラムが動作した。

なお、最初の画像の上2行は、月をどこで切るかを自分で判断したかったので、その部分の操作はプログラムに入っていない。また、インデントをちょこちょこ調整しないといけないのは、ちょっと面倒かもしれない。

とはいえ、Excelで「31/Okt 2024 Do」などの文字列を作るところからやるよりも、作業は数百倍は楽だ。生成AIがプログラミングに使えるようになって、本当に良かったと思う。

この記事が気に入ったらサポートをしてみませんか?