【JavaScript】ストップウォッチを作ってみた

# 概要
画面自体はいらないんだけど、JSの時間を測ったりで何だかんだよく使うので自分でコピペする為に作った。
丸コピしてくれてもいいし、クソコードと反面教師にしてもいい。

# 動作サンプル
https://jsfiddle.net/mahny/wdkzjf8v/107/

# 機能
- ラップがないストップウォッチと同等（スタート、ストップ、リセット）
- DIV等の任意のタグに計測時間の表示を行う
- 計測時間の更新間隔をms単位で設定できる
    - スタート／ストップの精度とは別なので例え10秒間隔にしてもミリ秒で時間を記録する
- いつでもインスタンスから計測時間（ms）を取得できる
- いつでも計測時間の文字列表現（00:00:00.000の形）を取得できる

# 使い方の例
インスタンスを作って、`startOrStop()`と`reset()`を呼ぶためのハンドラを設ければいい。
HTMLは必須ではないが、本クラスで画面表示までさせるなら以下のように計測時間表示用のタグを設ければいい。

```html
<div id="stopwatch">00:00:00.000</div>
<button onmousedown="startStop()">START/STOP</button>
<button onmousedown="reset()">RESET</button>
```

```js
// stopwatchというidを持つタグを探して、そこに時間を表示するようになる。
let sw = new StopWatch('stopwatch', 50);

function startStop() {
  sw.startOrStop();
}
function reset() {
  sw.reset();
}
```

# コード
```js
/**
 * ストップウォッチクラス
 * @auther mahny
 * @copyright mahny
 * @license MIT License
 */
class StopWatch {
  /**
   * 指定したIDを持つタグに対してストップウォッチ処理を行う
   *
   * @params {string} displayId
   *   時間表示を行うタグのID
   * @params {number} interval
   *   タイマー表示の更新間隔(ms)
   * @throws 指定したIDを持つタグが見つからない時
   * @throws 指定した更新間隔が正の数値以外の時
   */
  constructor(displayId, interval = 100) {
    this._displayElement = document.getElementById(displayId);
    if ((typeof(interval) !== 'number') || interval < 1) {
    	throw "指定した更新間隔は正の数値ではない。 / interval=[" + interval + "]";
    }
    this._interval = Math.floor(interval);
    this._startTime = 0;
    this._totalTime = 0;
  }

  /**
   * 計測時間を取得する
   * @returns {number} 計測時間（ms）
   */
	get totalTime() {
    let result = 0;
    switch (this.getStatus()) {
      case 'start':
        result = this._totalTime + (new Date().getTime() - this._startTime);
        break;
      case 'stop':
        result = this._totalTime;
        break;
      default:
        // nop
    }
    return result;
  }

  /**
   * 計測時間の文字列表現を返す
   * @returns {string} 計測時間の文字列表現
   */
  toString() {
    let result = '00:00:00.000';
    let restTotalTime = this.totalTime;

    let hour = Math.floor(restTotalTime / 3600000);
    let min = Math.floor(restTotalTime / 60000) % 3600;
    let sec = Math.floor(restTotalTime / 1000) % 60;
    let msec = restTotalTime % 1000;
    result = ('0' + hour).slice(-2) +
      ':' + ('0' + min).slice(-2) +
      ':' + ('0' + sec).slice(-2) +
      '.' + ('00' + msec).slice(-3);
    return result;
  }

  /**
   * ストップウォッチの状態を取得する
   *
   * @returns {string} ストップウォッチの状態
   */
  getStatus() {
    if (0 < this._startTime) {
      return 'start';
    } else if (0 < this._totalTime) {
      return 'stop';
    } else {
      return 'init';
    }
  }

  /**
   * 時間表示タグの表示時間を現在値に更新する
   */
  _refresh() {
  	if (!this._displayElement) {
      return;
    }
    this._displayElement.innerHTML = this.toString();
  }

  /**
   * 計測を開始または再開する
   */
  _start() {
    // 画面更新用関数（ループ）
    let self = this;
    let masureTimeFunc = function() {
      if (self.getStatus() === 'start') {
        self._refresh();
        setTimeout(masureTimeFunc, self._interval);
      }
    }
    this._startTime = new Date().getTime();
    setTimeout(masureTimeFunc, this._interval);
  }
  
  /**
   * 計測を停止する
   */
  _stop() {
    this._totalTime += new Date().getTime() - this._startTime;
    this._startTime = 0;
    this._refresh();
  }

  /**
   * 状態によって、計測を開始／再開または停止をする
   */
  startOrStop() {
    switch (this.getStatus()) {
    case 'start':
    	this._stop();
      break;
    default:
      this._start();
    }
  }

  /**
   * 状態によって、計測時間をリセットを行う
   */
  reset() {
    let status = this.getStatus();
    if (status === 'stop') {
			this._startTime = 0;
			this._totalTime = 0;
      this._refresh();
    } else {
      console.log('停止状態でない為、リセットできません。 / status=[' + status + ']');
    }
  }
}
```

# 追記
- 2021/01/18
    - vf8974 さんよりコメントでonclickだと指を押して話すまでのラグが出ちゃうよという話だったのでHTMLのサンプルを修正した。ありがとうございます。
    - 画面必須じゃない事を思い出したので、id指定は任意にした。挙動の違いは動作サンプルで確認して欲しい