🕑️

時刻差分のアルゴリズム:日をまたぐ時間差も考慮した正確な計算方法

に公開

ざっくり先に確認しておきたいこと

  • アルゴリズムって何?(※別の記事にとびます)
    「アルゴリズムって聞いたことあるけど、なんだっけ…?」という人向けに、やさしくまとめてます。
  • この記事の進め方について(※別の記事にとびます)
    どういう流れで説明してるか、ざっくり紹介してます。
    読む順番が気になる人はちらっとどうぞ。

はじめに

「ログ見ると、開始と終了の時刻はあるけど、差分は自分で計算しないと…」
「23:55から00:10って、15分だよな…? うっかり間違えそう」

こんなふうに、時刻の差を計算する処理って、地味だけどあちこちで出てきます。
今回は、時刻差分のアルゴリズムをコードと一緒に紹介していきます。

このアルゴリズムで出来ること

hh:mm:ss(時:分:秒)形式で指定された2つの時刻の差分を求める

動作順序

①2つの指定時刻をそれぞれ[時間,分,秒]に分ける
②2つの時刻の[時間の差,分の差,秒の差]を求める
③時間差を3600倍,分差を60倍する
④すべての差を合計すると[秒]の時刻差分が求まる
⑤時刻表示とするため、まずは差分を3600で割り、その商が[時間]部分になる
⑥⑤の余りをあらたに時刻差分とする
⑦この時刻差分を60で割った商が[分]になり、余りが[秒]部分になる

時刻差分の動作概要

計算対象選別
判定 計算対象 関係 数値
データの比較回数 = 0
データの交換回数 = 0
関数の呼び出し回数 = 1
変数定義
変数にする対象 変数 時間
1時間 h 1×h 60×h 60×60×h
1分間 m 1×m 60×m
1秒間 s 1×s
1つ目の時刻 a_1
2つ目の時刻 a_2
秒の時刻差分 S_t

一般化

1つ目の時刻の秒部分は s(a_1)hh:mm:ss 形式で下2桁にあたる部分であり
それをふまえて1つ目の時刻全体を秒であらわすと

a_1=[60×60×h(a_1)]+[60×m(a_1)]+[1×s(a_1)]
共通因数でくくると
a_1=60\{[60×h(a_1)]+m(a_1)\}+s(a_1)
同様に2つ目の時刻を秒であらわすと
a_2=60\{[60×h(a_2)]+m(a_2)\}+s(a_2)
a_2>a_1 のとき S_t=a_2-a_1 なので
S_t=60\{⟮60×[h(a_2)-h(a_1)]⟯+[m(a_2)-m(a_1)]\}+[s(a_2)-s(a_1)]
ここまでで秒の時刻差分が求まり
これは hh:mm:ss 形式で言うと以下のようになる
{\frac {S_t} {3600}\ 時間\ \frac {S_t\%3600} {60}\ 分\ (S_t\%3600)\%60\ 秒}
この式は計算量を指し示しておらず時刻差分を求めるための文字式でしかない

別途で計算量を表現するならば上記のように

  • 秒の時刻差分 S_t を求める
  • 求めた時刻差分を時刻表示にして出力する

この2つの処理が可能な関数を呼び出すことになる
※ここで関数の引数は2つの時刻 a_1a_2 である

よって計算量としては関数を1回呼び出すことに等しいので
T(n)=1
漸近計算量は O(1) である

実装

Python

def parse_time(time_str):
    h, m, s = map(int, time_str.split(":"))
    return h, m, s

def to_seconds(h, m, s):
    return h * 3600 + m * 60 + s

def from_seconds(total_seconds):
    if total_seconds < 0:
        total_seconds += 24 * 3600
    total_seconds = int(total_seconds)
    h = total_seconds // 3600
    total_seconds %= 3600
    m = total_seconds // 60
    s = total_seconds % 60
    return f"{h:02}:{m:02}:{s:02}"

def main(start, end):
    h1, m1, s1 = parse_time(start)
    h2, m2, s2 = parse_time(end)
    sec1 = to_seconds(h1, m1, s1)
    sec2 = to_seconds(h2, m2, s2)
    diff = sec2 - sec1
    return from_seconds(diff)

入力

10:00:00
13:00:00

実行結果

実行時間    : 0.000058 秒
メモリ(現時点): 49 bytes
メモリ(ピーク): 339 bytes
実行結果    : 03:00:00

理屈を押さえておくと、こういう応用がしやすくなる
ただ引き算するだけじゃなくて…

  • 時刻の順番が逆だとどうなる?
  • 時間帯(タイムゾーン)が違ったら?
  • 日付をまたいでたら?
  • 秒単位で見る?分単位?それとも日数?

こんな「ちょっとした落とし穴」も、仕組みを知っていれば柔軟に対応できる。
コードを書く人だけじゃなくて、データを扱う仕事なら、誰でも知っておいて損はないアルゴリズム。

応用場面

「ふたつの時刻の差を計算する」──たったそれだけの処理でも、ちょっとしたアイデアと組み合わせると、いろんなところで使えるようになる。

  1. イベントや作業の遅れを検出できる
    例えば、何かの処理が「5分以内に終わるはず」と決まっていたら、その始まりと終わりの時刻を比べるだけで、遅延の有無をチェックできる。
  2. ユーザーの反応時間を測れる
    アンケートの開始・終了、ログインしてから操作するまでの時間など、行動の速さや間を可視化できる。
    マーケティングやユーザー体験の分析にも活きる。
  3. 一定時間内の 空白(無反応) を検出できる
    センサーやログデータなどで、しばらく何も起きてない時間を見つけたいときに便利。たとえば「最後の記録から10分以上何も起きてない」ことに気づける。
  4. ピークの時間帯を見つけやすくなる
    たくさんの時刻データがあるなら、それぞれの差分から集中している時間帯や混んでいる時間帯が見えてくる。
    アクセス解析やシフト調整にも応用できる。
  5. 平均時間や最大・最小間隔を分析できる
    何かが何回も起きてるなら、その「間隔の平均」や「最も開いた時間」を出すことで、ばらつきやリズムが見えてくる。

比較検証

いろんなライブラリで時刻差分をとったときの実測値がこちら。

ライブラリ 実行時間 メモリ(現時点) メモリ(ピーク)
自作 0.0001秒 49B 339B
datetime 0.0038秒 615,243B 617,794B
dateutil 0.0155秒 1,395,562B 1,395,962B
pandas 0.6152秒 30,851,005B 30,885,673B
Python
#標準
def calc_datetime(start_str, end_str):
    from datetime import datetime
    fmt = "%H:%M:%S"
    start = datetime.strptime(start_str, fmt)
    end = datetime.strptime(end_str, fmt)
    diff = end - start 
    return from_seconds(diff.total_seconds())

#拡張
def calc_dateutil(start_str, end_str):
    from dateutil import parser
    start = parser.parse(start_str)
    end = parser.parse(end_str)
    diff = end - start
    return from_seconds(diff.total_seconds())

def calc_pandas(start_str, end_str):
    import pandas as pd
    start = pd.Timestamp(start_str)
    end = pd.Timestamp(end_str)
    diff = end - start
    return from_seconds(diff.total_seconds())

動作検証

参考サイト

GitHubで編集を提案

Discussion