かってに通知メモ v1.1.4 リリース:「夜の7時半」バグ修正と、同じバグが2箇所にあった話

「かってに通知メモ」v1.1.4 をリリースしました。今回も例の辛口の友人からの指摘で、「夜の7時半」が「19時0分」になってしまうバグを修正したものです。

修正自体はシンプルでしたが、調査の過程で「同じバグが2箇所にあった」という、ちょっと面白い発見があったので記録しておきます。

目次

報告されたバグ

友人からの指摘はこうでした:

「夜の7時半」と入力すると 19:00 になる。
単に「19時半」なら 19:30 になるのに。

確かに不便。前回 v1.1.3 で「8時半」のバグを直したのに、「夜の」を頭につけるだけでまた「半」が消える…という現象です。

実際にテストしてみると、こんな結果でした:

入力結果期待値
夜の7時半19:0019:30
朝の8時半8:008:30
夕方の5時半17:0017:30
今夜7時半19:0019:30

一方、なぜか OK なパターンもありました:

入力結果
お昼の12時半12:30 ✅
午前9時半9:30 ✅
午後3時半15:30 ✅
明日の朝8時半8:30 ✅

「OK パターン」と「NG パターン」を並べてみると、原因がだいたい見えてきました。

一回目の修正:実は届いていなかった

最初は _tryTomorrowTime というメソッドの中にバグを見つけました:

if (text.contains('夜') || text.contains('晩')) {
  int? hour = _extractHour(text);
  if (hour != null && hour < 12) hour += 12;
  hour ??= nightHour;
  return DateTime(tomorrow.year, tomorrow.month, tomorrow.day, hour, 0);
  //                                                                ↑
  //                                                  ハードコードされた 0
}

分の部分が 0 でハードコードされている!_extractMinute を呼んでいないので、「半」が無視されていたわけです。

ここを直して、実機テストしてみたら…結果が変わらない。

「あれ?コードは正しいはずなのに…」と思って、parse() メソッドの呼び出し順を確認したら:

final result =
    _tryRelativeMinutes(...) ??
    _tryRelativeHours(...) ??
    _trySpecificDate(...) ??
    _tryTodayTime(...) ??
    _tryTomorrowTime(...) ??     // ← ここに来る前提だったけど…
    _tryDayAfterTomorrow(...) ??
    _tryFuzzyTime(...) ??
    _tryVagueTime(...);            // ← 実はここで処理されていた

「夜の7時半」には「明日」というキーワードが含まれていないので、_tryTomorrowTime の冒頭で:

if (!text.contains('明日') && !text.contains('あした')) return null;

と即座に弾かれていました。つまり、修正は正しいけど、到達しないコードでした。

真犯人は _tryVagueTime

最終的に処理を引き受けていたのは _tryVagueTime でした。ここに同じパターンのバグが4箇所あったんです:

if (text.contains('夜') || text.contains('晩')) {
  ...
  return DateTime(base.year, base.month, base.day, hour, 0);  // ← ここ
}
if (text.contains('朝') || text.contains('午前')) {
  ...
  return DateTime(base.year, base.month, base.day, hour, 0);  // ← ここ
}
if (text.contains('昼') || text.contains('正午')) {
  return DateTime(base.year, base.month, base.day, noonHour, 0);  // ← ここ
}
if (text.contains('夕方')) {
  ...
  return DateTime(base.year, base.month, base.day, hour, 0);  // ← ここ
}

_tryTomorrowTime と同じ構造のバグが、別のメソッドにもそっくり存在していた、というわけです。

修正は同じパターンで、各箇所に:

final minute = _extractMinute(text);

を1行追加して、最後の 0 を minute に置き換えるだけ。4箇所すべて同じ修正で解決しました。

今回の学び:同じバグは複数箇所にあるかも疑う

これは個人開発あるあるかもしれませんが、似たような処理が複数のメソッドに散らばっていると、同じバグが複数箇所に存在することがあります。

今回の場合、_tryTomorrowTime と _tryVagueTime で「夜」「夕方」「朝」「昼」を別々に処理していました。コピペで作ったわけではないと思いますが、結果として同じパターンのコードが2箇所にあり、同じバグも2箇所にあった、という構造でした。

修正する時の教訓:

  • バグを直す前に、似た処理が他にないか grep してみる
  • 直した後、実際に動作確認するまで安心しない(私は最初の修正で「直った!」と思いそうになりました)
  • 「コードが正しいのに動作が変わらない」時は、そのコードに到達していない可能性を疑う

シンプルな話ですが、改めて意識すると良いポイントだと感じました。

1週間で3バージョンの怒涛のリリース

実は今回の v1.1.4 で、1週間で v1.1.2 → 1.1.3 → 1.1.4 と3バージョンを公開したことになります。

バージョン内容
v1.1.2ジャスト時刻通知、「そのうち」設定、通知動線の統一
v1.1.3「8時半」バグ修正、曖昧表現5つ追加
v1.1.4「夜の7時半」など時間帯+〜時半 バグ修正

これらすべて、辛口の友人からのフィードバックが起点でした。

普通なら「次の大型アップデートにまとめてリリース」と考えるところですが、個人開発の良いところは「気になったらすぐ直してすぐ届けられる」スピード感かもしれません。特に時刻認識のような 日常で頻繁に使う部分のバグ は、待たせる理由がないので、できるだけ早くリリースする方針でやっています。

小さい修正なら、審査も平均1日以内に通過してくれているので、フィードバック → 修正 → リリースのサイクルが本当に速い。

おわりに

今後は、引き続き「日本語に強いリマインダーアプリ」というポジションを大切にしながら、季節・祝日対応(ひな祭り、節分、バレンタイン、敬老の日など)を v1.2.0 として計画中です。

「これも認識してほしい」「こう書いたのにうまくいかない」というご報告、いつでも歓迎です。今回のように、些細に見える指摘が地味に改善につながります。

目次