JavaScript
tips

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

導入

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

今回は、JSONPに対応したNTPサーバにアクセスし、ページ読み込み時に日付のズレを自動調節するスクリプトを組みます。

注意

  • この方法は、"ほぼ"正確な日付を提供しますが、完全に正確な日付を取得するわけではありません。NTPサーバからデータを受信するまでに、どうしてもタイムラグが発生してしまうためです。
  • ページの閲覧中に日付設定を変更されると、再読み込みするまで正しい表示が出来なくなります。
  • この記事にて利用させて頂いているNTPサーバ【日本標準時プロジェクト】は試行中のものであり、いつ停止するか分かりません。

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

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

方法

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

日本標準時プロジェクト https/http を介してアクセスされる場合

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

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

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

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

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

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

参考: JavaScript - 多分正確な時刻をNTPから取る - Qiita

JSONPのやり方

※JSONPのやり方を知っている方は、ここを読み飛ばしてください

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

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

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

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

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

コード

サンプル

html
<div id="clock"></div>
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
 */
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;
    });
};