JavaScriptでコンピュータの日付設定に依存しない"ほぼ"正確な時計を作る

## 利用していた時刻配信サーバの停止（2020年12月24日追記）

この記事では、NICTの日本標準時プロジェクトで提供されていたHTTP/HTTPS経由の時刻配信サーバを利用していました。しかし、2020年7月14日のお知らせで、**HTTP/HTTPS経由の時刻配信サーバが停止されることが発表されました**。

[ネットワークを利用した時刻配信におけるNTPへの一元化 ～http/httpsを利用した時刻配信の停止に向けた取組み～ - 日本標準時グループ　新着情報](https://jjy.nict.go.jp/httphttps-index.html)

このため、この記事のコードによる「正確な時計」は今後、利用できなくなります。

[2020-12-24-added]: #%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6%E3%81%84%E3%81%9F%E6%99%82%E5%88%BB%E9%85%8D%E4%BF%A1%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E5%81%9C%E6%AD%A22020%E5%B9%B412%E6%9C%8824%E6%97%A5%E8%BF%BD%E8%A8%98

## 導入

JavaScriptで日付情報を扱う`Date`オブジェクトは、コンピュータ内部の日付設定に依存します。
つまり、コンピュータ側の日付設定が間違っていると、不正確な時計が出来てしまうのです。

今回は、JSONPに対応したNTPサーバにアクセスし、**ページ読み込み時に**日付のズレを自動調節するスクリプトを組みます。*[【2020年12月24日追記】ただし、このサーバは今後停止されるため、利用できなくなります。][2020-12-24-added]*

### 注意

*   この方法は、"ほぼ"正確な日付を提供しますが、完全に正確な日付を取得するわけではありません。NTPサーバからデータを受信するまでに、どうしてもタイムラグが発生してしまうためです。
*   ページの閲覧中に日付設定を変更されると、再読み込みするまで正しい表示が出来なくなります。
*   この記事にて利用させて頂いているNTPサーバ[【日本標準時プロジェクト】](https://web.archive.org/web/20160829002835/http://www2.nict.go.jp/aeri/sts/tsp/PubNtp/clients.html)は<del>試行中のものであり、いつ停止するか分かりません</del>[**今後、停止されます【2020年12月24日追記】**][2020-12-24-added]。

    > ポーリング間隔は16秒以上開けてください。

    といった記述もあり、多量のアクセスにも対応していないようです。
    **実用案件、特に、金銭的損害の発生するような案件でこのサンプルを採用しないでください**。

## 方法

JSONPに対応したNTPサーバとして、今回は以下のサービスを利用します。

[日本標準時プロジェクト　https/http を介してアクセスされる場合](http://www.nict.go.jp/JST/http.html)

※[**【2020年12月24日追記】このサービスは今後、停止されます。**][2020-12-24-added]利用しないようご注意ください。

`http://ntp-a1.nict.go.jp/cgi-bin/jsont`にアクセスすると、`jsont`関数にオブジェクト形式のデータが渡されます。
関数名の変更は**出来ません**。この名前に固定されているようです。

渡されるデータには以下のパラメータが含まれています。

|パラメータ|内容|例|
|:--|:--|:--|
|`id`|サーバID<br>(サーバのホスト名)|`"ntp-a1.nict.go.jp"`|
|`it`|発信時刻<br>(指定された場合。未指定だと`0.000`となる)|`1232963971.248`|
|`st`|サーバ時刻|`1232963971.920`|
|`leap`|`next` 以前の時点での UTC と TAI の差(秒)|`33`|
|`next`|次、または最後のうるう秒イベント時刻|`1230768000`|
|`step`|次、または最後のうるう秒イベントが<br>挿入の場合 1、削除の場合 -1|`1`|

使うのは、サーバ時刻`st`と発信時刻`it`です。

NTPサーバからデータを受信したところでブラウザの時刻とサーバ時刻との差分を取り、
以降、差分を適用した時刻を元にする`Date`オブジェクトでの時計表示を行います。

NTPサーバでの時刻生成からブラウザでの受信完了までにかかった時間は、$(受信時刻 - 発信時刻) / 2$として計算します。

参考: [JavaScript - 多分正確な時刻をNTPから取る - Qiita](http://qiita.com/hashrock/items/ce686c5b38d82be16390)

### JSONPのやり方

※JSONPのやり方を知っている方は、[ここを読み飛ばしてください](#%E3%82%B3%E3%83%BC%E3%83%89)。

NTPサーバには、JSONPでのアクセスを行います。
これは、決して難しいものではありません。`script`要素を作成し、その`src`属性にNTPサーバのURLを指定するだけです。

JSONPの内容は、特定の関数に引数としてJavaScriptオブジェクトが指定されただけの、ただのJavaScriptコードです。
例えば、今回のNTPサーバの受信内容は

```js
jsont( {
 "id": "ntp-a1.nict.go.jp",
 "it": 1232963971.248,
 "st": 1232963971.920,
 "leap": 33,
 "next": 1230768000,
 "step": 1
} )

```

こうなります。
これをJavaScriptファイルとして読み込むと、`jsont`関数が実行され、その第一引数にデータが渡される。と、そういうことになります。

そこで、`jsont`関数の中に読み込み完了時の処理を書き込んで予め定義したあと、
`script`要素を作成してファイルを読みこめば受信できます。

## コード

※[【2020年12月24日追記】**このコードで利用している時刻配信サーバは今後、停止されます。**][2020-12-24-added]

[サンプル](http://jsbin.com/yiriji)

```html:html
<div id="clock"></div>
```

```js:JavaScript
/**
 * Date.nowに対応していない場合、定義する
 * @link https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility
 */
if (!Date.now) {
    Date.now = function now() {
        return new Date().getTime();
    };
}

/**
 * 時計表示要素を取得
 */
var clockE = document.getElementById('clock');

/**
 * 受信用のjsont関数を定義
 */
window.jsont = function (data) {
    var nowDate = Date.now();

    /**
     * 取得した標準時とコンピュータ側の時間との差を取得
     * @link http://qiita.com/hashrock/items/ce686c5b38d82be16390
     */
    var dateDiff = ((data.st * 1000) + ((nowDate - (data.it * 1000)) / 2)) - nowDate;

    /**
     * 時刻表示用関数を定義し、実行
     */
    function clock() {
        /**
         * 標準時とコンピュータ内蔵時計との差を考慮した
         * "ほぼ"正確な日付オブジェクトを取得
         */
        var date = new Date(Date.now() + dateDiff);

        /**
         * 時計を表示
         */
        setText(
            clockE,
            datePrintf('%y年%m月%d日 %h時%i分%s.%u秒', date)
        );

        /**
         * タイマーを設定し、50ミリ秒後に時計を更新
         */
        setTimeout(clock, 50);
    }
    clock();

    /**
     * jsont関数を削除
     */
    if (!(delete window.jsont)) {
        window.jsont = undefined;
    }
};

/**
 * script要素を作成し、NTPサーバのデータを取得する
 * NTPサーバは複数の中からランダムに選択し、負荷分散を試みる
 * @link http://bashalog.c-brains.jp/14/03/05-100000.php
 *
 * Note: 今後、この時刻配信サーバは停止されるため、利用しないでください。
 *       @link https://jjy.nict.go.jp/httphttps-index.html
 */
var serverList = [
    'https://ntp-a1.nict.go.jp/cgi-bin/jsont',
    'http://ntp-a1.nict.go.jp/cgi-bin/jsont',
    'https://ntp-b1.nict.go.jp/cgi-bin/jsont',
    'http://ntp-b1.nict.go.jp/cgi-bin/jsont'
];
var scriptE = document.createElement('script');
// NTPサーバのURLに発信時刻を追加
var serverUrl = serverList[Math.floor(Math.random() * serverList.length)];
scriptE.src = serverUrl + '?' + (Date.now() / 1000);
document.body.appendChild(scriptE);





/**
 * 数値のゼロ埋めをする
 *
 * @param {number} number 対象の数値
 * @param {number} digit 必要な桁数
 * @return {string} ゼロ埋めされた文字列
 */
var zeroFill = function (number, digit) {
    return ('00' + number).slice(digit * -1);
};

/**
 * 要素にテキストを設定する。textContentプロパティのクロスブラウザ関数
 * @param {Node} targetNode 設定対象の要素
 * @param {string} text 設定するテキスト
 */
var setText =
    ('textContent' in document.documentElement) ?
    function (targetNode, text) {
        /**
         * 対象要素のtextContentプロパティに新しいテキストを代入
         */
        targetNode.textContent = text;
    } :
    function (targetNode, text) {
        /**
         * 対象要素内の全子要素を削除
         */
        var childNode;
        while ((childNode = targetNode.firstChild)) {
            targetNode.removeChild(childNode);
        }
        /**
         * テキストノードを挿入
         */
        targetNode.appendChild(
            document.createTextNode(text)
        );
    }
;

/**
 * 指定の文字列をフォーマットし、日付文字列に変換する
 * フォーマットパラメータは以下の8つがある
 * + %y : 年
 * + %m : 月
 * + %d : 日
 * + %h : 時
 * + %i : 分
 * + %s : 秒
 * + %u : ミリ秒
 * + %% : ただの"%"
 *
 * @param {?string=} format フォーマット対象の文字列。
 * 省略された場合、"%y/%m/%d %h:%i:%s.%u"になる
 * @param {?Date=} date 日付を指定する場合、ここに日付オブジェクトを指定する
 * @return {string} フォーマットされた文字列
 */
var datePrintf = function (format, date) {
    if (!format) {
        format = '%y/%m/%d %h:%i:%s.%u';
    }
    if (!(date instanceof Date)) {
        date = new Date();
    }
    var
        year = date.getFullYear(),
        month = zeroFill(date.getMonth() + 1, 2),
        date_n = zeroFill(date.getDate(), 2),
        hour = zeroFill(date.getHours(), 2),
        minute = zeroFill(date.getMinutes(), 2),
        second = zeroFill(date.getSeconds(), 2),
        milli_second = zeroFill(date.getMilliseconds(), 3)
    ;
    return format.replace(/(%*)%([ymdhisu])/g, function (a, escape_str, type) {
        if (escape_str.length % 2 === 0) {
            switch (type) {
                case 'y':
                    type = year;
                    break;
                case 'm':
                    type = month;
                    break;
                case 'd':
                    type = date_n;
                    break;
                case 'h':
                    type = hour;
                    break;
                case 'i':
                    type = minute;
                    break;
                case 's':
                    type = second;
                    break;
                case 'u':
                    type = milli_second;
                    break;
            }
        }
        return escape_str.replace(/%%/g, '%') + type;
    });
};
```