star back image
people4
電飾 電飾
moon
men

オフラインのカレンダーを作る

BLOG WEBログ
読了約:33分

AI界の一角、Gemini Advancedを使う機会がありました。
何を作ろうか悩んだ挙句に、カレンダーを作ろうと思い至りました。

Advancedということは課金された状態ですね。

そのAdvancedです!

【経緯】なぜカレンダーなのか

古い記憶をだどります。

派遣切りで溢れた事務所で、社内のプログラム勉強会がありました。
その題目が「カレンダーを作る」と言うものでした。

なぜカレンダーのプログラムなの?と聞くと、
それは家電などの組み込みに使用される基本的なプログラムであるということ。

当時はMicrosoft Visual Studioを使いデスクトップアプリ化させる方法を学んだ後、自由作業として半日ほど放り出されました。

Javaの本が一冊、10人ほどで回し読み。

文字を並べて折り返してカレンダーのような見た目はできたのですが、機能させる方法が全くわかりませんでした。
それをできる人が適性がある人ということで、私は不合格。

それ以来プログラムとは無縁で、釈然としない人生を送っておりました。

しかし今はAIがあります。
記憶を辿り思い出した要件は以下です。

  • 西暦でやる
  • 最初は誰かが日付と時間を入力し動作させる
  • カレンダーの見た目を簡単に再現する
  • 好きなところに日付けと時間を配置する
  • 閏年も考慮したカレンダーであること
  • インターネットがない場所で動作すること
当時私はプログラムの基礎も知らず、閏年についても気にしたこともありませんでした。

カレンダーはシンプルに見えて実際には日付計算や閏年の考慮が必要で、プログラムの基礎を押さえていないと難しい課題のようです。

カレンダーの機能を作ることがプログラム適性の試金石とされたのです。

手も足も出ず、惨めでした。
基礎さえ出来ていれば働き口は沢山あったようです。

勉強不足であり、身の程知らずですぞ。

ちなみに出来た人は、一人だけいました。すげぇ。
聞くと経験者。それでもすごいと思いました。

【共有1】サンプルページとオフラインカレンダーのポイント

タイムゾーンもネットも不要!自分だけのオフラインカレンダー。

作ってみたかった「タイムマシン用のカレンダー」です。

タイムマシン?!

オフラインカレンダー(サンプルページ)
https://astrowave.jp/amnesia_record/calendar.php

「え、カレンダーなんてスマホにもあるし、手帳もあるじゃん?」って思いました? 確かにそうなんですけど、今回作ったのはちょっと違うんです。

なんと、タイムゾーンを気にしなくていい。 そして、インターネットに依存しないシンプルなツール。 (*゚∀゚)ノアヒャヒャ

その場所、その時間だけで動く、超ローカルなカレンダーです。

今の時間をセットして、未来の時間をセットします。

目標日付はオプションです。入れたらメッセージが出ます。

※「設定日」を変更し直したら「目標日付」はリセットされます。またやりたいときはもう一度設定してください。

①「タイム回路」と「次元転移装置」に1.21ジゴワットの電力を供給すること。②時速140kmで走ること。

面白そうにカッコよく言いたかった。

このオフラインカレンダーは「過去へ目標を設定」できません。
「設定日」で現在時刻を過去で入力すると、その当時の曜日を知ることが可能です。

時間は未来から来て、過ぎ去っていきます。
未来を決めるのはあなた!

発展させればこんな時に便利かも?

  • 海外旅行で現地の時間を確認したいけど、ネット環境がない… → オフラインカレンダーなら、到着した時間に合わせて設定すればOK!
  • 特定のイベントまでのカウントダウン表示に。 → 目標日を設定しておけば、ワクワクしながら当日を待てますね!
  • シンプルなタイマーとして。 → 目標時間を設定しておけば、時間になったらアラーム代わりにメッセージを表示させることも。
  • インターネットに接続しない環境で、日付や時間を管理したい場合に。

プログラムは初心者のようなもの。何度も修正して、ようやく今の形になりました。
まだ変なところあるかも。。

カレンダー表示って必要なくないですか?

いや、必要でしょう。
あった方がカッコいいです(#^ω^)

【共有2】ソースコード

HTMLのUIで基礎的な動作。
好きな場所にコピペして貼り付けて使えます。

電源さえあれば腕時計のように働き、目標時刻を登録すれば、その時刻にアクションを起こします。

<style>
  .highlight {
      background-color: yellow;
  }

  .goal-highlight {
      background-color: lightblue;
  }

  .both-highlight {
      background-color: red;
  }
</style>

<h1>オフラインカレンダー</h1>
<div>
  <label for="dateInput">設定日:</label>
  <input type="date" id="dateInput">
  <label for="timeInput">設定時間:</label>
  <input type="time" id="timeInput">
  <button id="setDateTimeButton">変更</button>
</div>
<p id="setDate">[設定した年月日]</p>
<p id="setTime">[設定した時間]</p>
<p id="currentDate">[設定からの年月日]</p>
<p id="currentTime">[設定からの時間]</p>
<div>
  <label for="goalDateInput">目標日付:</label>
  <input type="date" id="goalDateInput">
  <label for="goalTimeInput">目標時間:</label>
  <input type="time" id="goalTimeInput">
  <button id="setGoalButton">目標設定</button>
</div>
<p id="goalDate">[目標の日付]</p>
<p id="goalTime">[目標の時間]</p>
<p id="remainingTime">[目標までの残り日時間]</p>
<div class="calendar-container">
  <table border="1">
      <thead>
          <tr>
              <th>日</th>
              <th>月</th>
              <th>火</th>
              <th>水</th>
              <th>木</th>
              <th>金</th>
              <th>土</th>
          </tr>
      </thead>
      <tbody id="calendarBody"></tbody>
  </table>
</div>
<p id="message"></p>
<script>
  let selectedDateTime = null;
  let goalDateTime = null;
  let isPastGoalError = false; // フラグを追加
  let currentYear = null;
  let currentMonth = null;

  document.getElementById("setDateTimeButton").addEventListener("click", function() {
      const dateValue = document.getElementById("dateInput").value;
      const timeValue = document.getElementById("timeInput").value;

      if (!dateValue || !timeValue) {
          document.getElementById("message").textContent = "日付と時間を設定してください。";
          return;
      }

      // タイムゾーンを考慮しないDateオブジェクトを作成
      const [year, month, day] = dateValue.split('-').map(Number);
      const [hours, minutes] = timeValue.split(':').map(Number);
      selectedDateTime = new Date(year, month - 1, day, hours, minutes, 0);

      document.getElementById("setDate").textContent = "[設定した年月日] " + dateValue;
      document.getElementById("setTime").textContent = "[設定した時間] " + timeValue;
      document.getElementById("currentDate").textContent = "[設定からの年月日] " + dateValue;
      document.getElementById("currentTime").textContent = "[設定からの時間] " + timeValue;
      document.getElementById("message").textContent = "";
      goalDateTime = null;
      isPastGoalError = false; // フラグをリセット
      currentYear = selectedDateTime.getFullYear();
      currentMonth = selectedDateTime.getMonth();
      generateCalendar(currentYear, currentMonth);
  });

  document.getElementById("setGoalButton").addEventListener("click", function() {
      const goalDateValue = document.getElementById("goalDateInput").value;
      const goalTimeValue = document.getElementById("goalTimeInput").value;

      if (!goalDateValue || !goalTimeValue) {
          document.getElementById("message").textContent = "目標の日付と時間を設定してください。";
          return;
      }

      // タイムゾーンを考慮しないDateオブジェクトを作成
      const [year, month, day] = goalDateValue.split('-').map(Number);
      const [hours, minutes] = goalTimeValue.split(':').map(Number);
      goalDateTime = new Date(year, month - 1, day, hours, minutes, 0);

      if (!selectedDateTime) {
          document.getElementById("message").textContent = "設定日を入れてください。";
          return;
      }

      if (selectedDateTime && goalDateTime < selectedDateTime) {
          document.getElementById("message").textContent = "過去の目標時間は設定できません。";
          isPastGoalError = true; // フラグを設定
          return;
      }

      document.getElementById("goalDate").textContent = "[目標の日付] " + goalDateValue;
      document.getElementById("goalTime").textContent = "[目標の時間] " + goalTimeValue;
      document.getElementById("message").textContent = "";
      isPastGoalError = false; // フラグをリセット
      generateCalendar(currentYear, currentMonth);
  });

  function generateCalendar(year, month) {
      const calendarBody = document.getElementById("calendarBody");
      calendarBody.innerHTML = "";
      const firstDay = new Date(year, month, 1).getDay();
      const lastDate = new Date(year, month + 1, 0).getDate();
      let date = 1;
      for (let i = 0; i < 6; i++) {
          let row = document.createElement("tr");
          for (let j = 0; j < 7; j++) {
              let cell = document.createElement("td");
              if (i === 0 && j < firstDay) {
                  row.appendChild(cell);
              } else if (date > lastDate) {
                  row.appendChild(cell);
              } else {
                  cell.textContent = date;
                  // タイムゾーンを考慮しないDateオブジェクトを作成して比較
                  const currentDate = new Date(year, month, date);
                  const selectedDateOnly = selectedDateTime ? new Date(selectedDateTime.getFullYear(), selectedDateTime.getMonth(), selectedDateTime.getDate()) : null;
                  const goalDateOnly = goalDateTime ? new Date(goalDateTime.getFullYear(), goalDateTime.getMonth(), goalDateTime.getDate()) : null;

                  cell.className = "";
                  if (selectedDateOnly && currentDate.toDateString() === selectedDateOnly.toDateString()) {
                      cell.classList.add("highlight");
                  }
                  if (goalDateOnly && currentDate.toDateString() === goalDateOnly.toDateString()) {
                      cell.classList.add("goal-highlight");
                  }
                  if (selectedDateOnly && goalDateOnly && currentDate.toDateString() === selectedDateOnly.toDateString() && currentDate.toDateString() === goalDateOnly.toDateString()) {
                      cell.classList.add("both-highlight");
                  }
                  row.appendChild(cell);
                  date++;
              }
          }
          calendarBody.appendChild(row);
          if (date > lastDate) break;
      }
  }

  setInterval(() => {
      if (selectedDateTime && !isPastGoalError) { // フラグをチェック
          selectedDateTime.setSeconds(selectedDateTime.getSeconds() + 1);

          // 日付が変わったときの処理を修正
          const currentDateText = `${selectedDateTime.getFullYear()}-${String(selectedDateTime.getMonth() + 1).padStart(2, '0')}-${String(selectedDateTime.getDate()).padStart(2, '0')}`;
          const currentTimeText = `${String(selectedDateTime.getHours()).padStart(2, '0')}:${String(selectedDateTime.getMinutes()).padStart(2, '0')}:${String(selectedDateTime.getSeconds()).padStart(2, '0')}`;
          document.getElementById("currentDate").textContent = "[設定からの年月日] " + currentDateText;
          document.getElementById("currentTime").textContent = "[設定からの時間] " + currentTimeText;

          // 日付が変わったかチェック
          if (selectedDateTime.getFullYear() !== currentYear || selectedDateTime.getMonth() !== currentMonth) {
              currentYear = selectedDateTime.getFullYear();
              currentMonth = selectedDateTime.getMonth();
              generateCalendar(currentYear, currentMonth);
          }

          // 目標までの残り時間を計算して表示
          if (goalDateTime) {
              const remainingTime = goalDateTime.getTime() - selectedDateTime.getTime();
              if (remainingTime > 0) {
                  const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
                  const hours = Math.floor((remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
                  const minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
                  const seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);

                  document.getElementById("remainingTime").textContent = "[目標までの残り日時間] " + days + "日 " + String(hours).padStart(2, '0') + "時間 " + String(minutes).padStart(2, '0') + "分 " + String(seconds).padStart(2, '0') + "秒";

              } else {
                  document.getElementById("remainingTime").textContent = "[目標までの残り日時間] 0日 00時間 00分 00秒";
                  document.getElementById("message").textContent = "目標時間になりました!";
              }
          } else {
              document.getElementById("remainingTime").textContent = "[目標までの残り日時間] --日 --時間 --分 --秒";
          }
      }
  }, 1000);
</script>
AIにして欲しかったけど説明が難しかったこと。

時間の管理はローカルの設定に基づく:
Date オブジェクトはローカル時間に依存するので、ローカル設定を手動で変更する前提となります。もし日付や時刻の更新が必要な場合、それをスクリプト内で制御します。

タイムゾーンを無視した動作:
タイムゾーンやネットワーク依存の要素を排除し、完全にローカルでの設定で管理します。つまり、スクリプト内で設定した日時(selectedDateTime)を基準にして、すべての処理が行われます。

時間の進行や日付跨ぎの処理:
ローカルの設定日時に基づき、1秒単位で時間を進め、日付の変更を行うようにします。日付が変更されたタイミングで、カレンダーや表示が更新されるように。

AIの大きな可能性を感じつつ、一息ついていたら。なんと。

AIから2点ほど提案をいただきました。

🔧 もし手を加えるならこんな感じもおすすめ(任意):

1. setInterval の generateCalendar 呼び出し頻度を抑える(秒単位では重いかも)
例えば、日が変わったときだけ再描画にしてもOKです。今の秒数更新も素敵ですが、端末スペックやバッテリーの都合で負荷を下げたいならアリです。

2. 時間の差分表示(「あと○分」など)
将来的に「目標まであと何分」みたいな表示があったら、さらに楽しいかも。

要件を2つ追加しました。

  • カレンダーは日が変わったときだけに再描画にする
  • 「目標まであと何分」みたいな表示を追加

[目標までの残り日時間]を追加しました。

オフラインカレンダーサンプルページを更新しております。

閏年の仕組みは入っているように見えなかったので調べました

閏年の仕組みは入っているように見えなかったので調べました。
ない?AIに聞きました。

JavaScriptの Date オブジェクトは、日付と時間を扱うための組み込みオブジェクトであり、閏年の判定を自動的に行います。

具体的には、generateCalendar 関数内で、月の最終日を取得するために以下のコードを使用しています。

以下がDate オブジェクトその部分です。

const lastDate = new Date(year, month + 1, 0).getDate();

ちゃんと理解してますか?

さらによくばって組み込み系でできるのかをAIに尋ねました。

組み込み系で他のプログラム言語にしたいとGeminiに尋ねたら、どうやら一筋縄では行かないようです。C/C++、Pythonが候補ですが、違うライブラリを使ったり組み込みさきのメモリ効率も考えないといけないようです。

組み込み環境のディレクションと、ターゲット言語の知識が自分にはありません。。
Arduinoやラズパイに液晶をつけてやるイメージでしょうか。
それは気が向いたら調べようかなと。

Scriptの解説ならGeminiやchatGPTに放り込んでみてどうぞ。

インターネットに依存しないシンプルなツールで、スタンドアローンな感じの、意外と可能性を秘めているかもな妄想をして、終わりとします。

参考サイト置き場

ツェラーの公式
https://ja.wikipedia.org/wiki/%E3%83%84%E3%82%A7%E3%83%A9%E3%83%BC%E3%81%AE%E5%85%AC%E5%BC%8F

【初心者向け】結局、Arduinoとラズパイって何が違うの?
https://qiita.com/akinami/items/f5b58689d546698d1ff9

【AI】イラストを描いてもらった

今回の記事のキャッチ画像で使わせてもらいます「Google ImageFX」で作成した画像です。誰でもgoogleアカウントでログインして使えます。

この記事にピッタリなイラストのための考えたリクエストは、「タイムトラベルのためのタイマー付き機材を腕につけて、今の時間と未来の時間をセットしている状況。男性キャラクターで身につけている装備はLEDが光っていて未来装備でタイトな服装です。タイマー付き機材は手首から腕ににかけての腕時計の位置付近。背景は都会の建物の陰。男性の後ろから腕の装置への時間入力しているシーンを見た状況。アップ寄りで。」です。

Memeplex.appが最近は人気なのかすごく待たされます。
5分くらいなら我慢できるのですが。。

星間旅路のメロディ

「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」

「この電波はどこの星からきたのだろうか。」

水の豊かな惑星の歌。
生命も輝いていた、そんな雰囲気がします。