torutkのブログ

ソフトウェア・エンジニアのブログ

Javaでの時間(絶対時刻、相対時刻=時間)をどう表現するかを考える

 一見たいした課題と思っていなかった時間の表現ですが、深入りしてはまってしまいました。Javaでストップウォッチ機能を持つ時計プログラムを作ってみようとしたところ、2つの時刻の差(時間)の表現で深みにはまってしまったのです。

時計に必要な機能

 まず、時計の基本機能として絶対時刻(協定世界時UTC)に極力近い時刻を取得・表示します。

  • 取得:まあ、コンピュータのシステムクロックを取ればよいでしょう。コンピュータがNTPでUTCに近い時刻サーバ(インターネット上のNTPサーバやGPS時計など)と同期していれば実用上まず問題はないでしょう。
  • 表示:UTC時刻に対して、その地域に合わせてオフセットした標準時を表示します。JavaならTimeZoneを扱うことができるので、これは問題なく実装できます。文字列化については、java.text.DateFormatterやjava.text.SimpleDateFormatterを使います。

 次に、ストップウォッチ機能ですが、これはユーザが開始操作した瞬間をゼロとしてそれ以降経過した時間を取得・表示します。

  • 計測開始した瞬間:これは開始操作時点の絶対時刻(システムクロック)を取得し保持しておきます。
  • 経過時間:計測開始の絶対時刻と現在の絶対時刻の差から計算します。Javaの場合はいったん基準時刻(エポック時刻:1970年1月1日0時0分0秒UTC*1)からの積算ミリ秒に変換し、これを引き算することで経過時間(ミリ秒)を算出します。
  • 表示:2つの絶対時刻の差から算出した「時間」は、「時刻」ではないので、時刻の表示と同じ仕組み(DateFormatter)ではNGです。ここに工夫が必要となります。

Java SE標準APIでの時刻・時間

Java SE標準APIでの現在時刻の取得

 Javaで時刻情報を表現する方法として、以下があります*2

  • long型によるエポック時刻からの通算ミリ秒
  • java.util.Date型
  • java.util.Calendar型

 現在時刻(システムクロック)を取得するには、

  1. SystemクラスのcurrentTimeMillis()メソッドでlong型のエポック時刻からの通算ミリ秒を取得
  2. Dateクラスのインスタンスを引数なしのコンストラクタで生成
  3. CalendarクラスのインスタンスをgetInstanceメソッドで取得

があります。

    // 1. 通算ミリ秒取得
    long nowInMillis = System.currentTimeMillis();
    // 2. Date型での現在時刻取得
    Date nowDate = new Date();
    // 3. Calendar型での現在時刻取得
    Calendar nowCalendar = Calendar.getInstance();

 なお、Dateクラスには、setTime(long)メソッドで通算ミリ秒を指定して時刻を変更することができます。Calendarクラスには、setTime(Date)メソッドでDateオブジェクトを指定して時刻を変更すること、および、setTimeInMillis(long)で通算ミリ秒を指定して時刻を変更することができます。

Java SE標準APIでの現在時刻の表示

 Javaで時刻を表示する方法として、以下があります。

  • java.util.DateFormat型によるDate型の時刻を文字列化
  • java.util.SimpleDateFormat型によるData型の時刻を文字列化
    DateFormat format = DateFormat.getTimeInstance(DateFormat.MEDIUM);
    String text = format.format(nowDate);

DateFormat.MEDIUMを指定した場合、ロケールによって表現が多少変わります。
日本や英国では、23:45:01 となりますが、米国では、11:45:01 PM となります。

    DateFormat format = new SimpleDateFormat("HH:mm:ss");
    String text = format.format(nowDate);

SimpleDateFormatクラスのインスタンスをコンストラクタに書式を指定して生成した場合は、ロケールによらず同じ表現となります。

Java SE標準APIでの時間(2つの時刻の差)の取得

 java.util.Date型もjava.util.Calendar型も、2つのオブジェクトの差を直接算出するメソッドは持ち合わせていません。そこで、いったん通算ミリ秒を取り出し、通算ミリ秒での差を計算することとなります。
 ここで問題となるのは、2つの時刻の差として計算した通算ミリ秒を今度はどうやって時分秒で表現するかです。Date型にこの2つの時刻の差である通算ミリ秒をセットすることもできますが、その場合Date型が表わすのは1970年1月1日0時0分0秒UTC時刻に計算された通算ミリ秒を加えた時刻となってしまいます。

 たとえば、2つの時刻差が600秒であったとき、これをDate型オブジェクトに設定すると、1970年1月1日0時10分0秒UTCとなります。TimeZoneが日本標準時UTC+9)でDateFormatなどで文字列化すると、9時10分0秒となります。

 つまり、時間(時刻の差)をDate型やCalendar型で表現するのは不適切ということになります。

Java SE標準以外の時刻・時間を扱うAPI

 Java SE標準以外に時刻・時間を扱う便利なライブラリがないか探してみました。

Apache commons Lang

org.apache.commons.lang.timeパッケージが提供されており、その中に

  • DateUtils
  • DateFormatUtils
  • DurationFormatUtils
  • FastDateFormat
  • StopWatch

といったクラスが用意されています。特にDurationFormatUtilsは、2つの時刻の差を文字列化(日時分秒)するユーティリティです。
StopWatchは今回作ろうと思っていたストップウォッチ機能に便利そうです。

Joda-Time

http://joda-time.sourceforge.net/index.html
下記のJSR-310に収斂されるようです。現在のバージョンで2つの時刻の差を表示するコードは以下のようになります。

import org.joda.time.*;
import org.joda.time.format.*;

public class TimeSample {
    public static final void main(final String... ignore) {
        // 2007年9月29日17時30分0秒(9月を9で設定可能!)
        DateTime start = new DateTime(2007, 9, 29, 17, 30,  0, 0);
        // 2007年9月29日18時25分30秒(9月を9で設定可能!)
        DateTime stop  = new DateTime(2007, 9, 29, 18, 25, 30, 0);
        // 2つの時刻の間(時間)を表わす
        Period period = new Interval(start, stop).toPeriod();

        DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("HH:mm:ss");
        String startText = dateTimeFormatter.print(start);
        String stopText  = dateTimeFormatter.print(stop);
        PeriodFormatter periodFormatter = new PeriodFormatterBuilder()
            .printZeroAlways()
            .minimumPrintedDigits(2)
            .appendHours()
            .appendSeparator(":")
            .appendMinutes()
            .appendSeparator(":")
            .appendSeconds()
            .toFormatter();
        String periodText = periodFormatter.print(period);
    }
} 

 java.util.Calendarのように月が0から始まるので9月の場合には8をsetするといった間違い誘発因子が改善されています。

 また、時間について専用の型IntervalとPeriodが用意されています。

JSR-310 Date and Time API

https://jsr-310.dev.java.net/

次のJava SEリリース(Java SE 7)を目指して新しいAPIの仕様策定が進んでいます。
上記のjoda-timeをベースにしているようですが、基本は新しいAPIを定義するようです。日付および時刻の表記に関する規格ISO 8601に準拠する方針のようです。

*1:GMTと表記すべきかもしれません。GMTというとタイムゾーンUTC+/-0の場所を指すのか天文学的観測による時刻を指すのかあいまいな気が・・・

*2:java.sql.DateはSQLデータベースとの接続で使用するJDBC APIのものなのでここでは除外