一見たいした課題と思っていなかった時間の表現ですが、深入りしてはまってしまいました。Javaでストップウォッチ機能を持つ時計プログラムを作ってみようとしたところ、2つの時刻の差(時間)の表現で深みにはまってしまったのです。
時計に必要な機能
まず、時計の基本機能として絶対時刻(協定世界時:UTC)に極力近い時刻を取得・表示します。
- 取得:まあ、コンピュータのシステムクロックを取ればよいでしょう。コンピュータがNTPでUTCに近い時刻サーバ(インターネット上のNTPサーバやGPS時計など)と同期していれば実用上まず問題はないでしょう。
- 表示:UTC時刻に対して、その地域に合わせてオフセットした標準時を表示します。JavaならTimeZoneを扱うことができるので、これは問題なく実装できます。文字列化については、java.text.DateFormatterやjava.text.SimpleDateFormatterを使います。
次に、ストップウォッチ機能ですが、これはユーザが開始操作した瞬間をゼロとしてそれ以降経過した時間を取得・表示します。
Java SE標準APIでの時刻・時間
Java SE標準APIでの現在時刻の取得
Javaで時刻情報を表現する方法として、以下があります*2。
現在時刻(システムクロック)を取得するには、
- SystemクラスのcurrentTimeMillis()メソッドでlong型のエポック時刻からの通算ミリ秒を取得
- Dateクラスのインスタンスを引数なしのコンストラクタで生成
- 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で時刻を表示する方法として、以下があります。
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が用意されています。