M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

“流れるようなインターフェース”で時間や日付の操作

ずいぶん前にこちらの記事からリンクをたどって見つけていたJavaのプロジェクトのイントロページを訳出してみました。原典は、“Time and Money Code Library”です。もともとあまり分かりやすい英語でないのもあって(言い訳)、かなり翻訳精度低いと思います。あしからず。

* * *

Time and Money Code Library

このプロジェクトは時間と金額のような繰り返し使用されるドメインについて基本的な概念の操作を行うコードを開発するものです。開発するにあたり準拠した設計原則は『ドメイン指向設計』(Domain-Driven Design)の第3編のなかで解説されています。著作権表示はこちらを、またリリースノートはこちらを参照してください。概念的な研究については、時間代数学の解説や区間数学を参照してください。

単体テストコードに加えて、”examples/src”フォルダ配下には”example.*”パッケージが格納されていて、多くのアプリケーションで登場する典型的な処理ロジックが実行可能なコードとして提供されています。(将来的にはもっと例を増やしたいところです。)使用方法についてはこのあと簡潔にお話ししましょう。こちらのページでよくある質問と回答をご覧になれます。

このソフトウェアは非常に自由なMITライセンスのもとで提供されているので(licence.txtを参照)、その使用を妨げるものはなにもありません。

Getting Started with Time Language

TimeLanguageは基本的な時間のモデルを具体化するクラスとインターフェースのセットで、時間値の表象と操作を可能にし、時間に関するものごとを表現する方法を提供します。Java標準ライブラリのDateクラスはいくつかの場合に本来簡単で明瞭であるべきものを難しく不明瞭にしてしまっています。

プロジェクトの当初の目標の1つはオブジェクトの生成を簡便に、そして読みやすくすることでした。日付値がもっぱらUI〔でのユーザの入力〕やデータベースからやって来てそれをプログラムで操作するようなアプリケーションの場合、上述の目標はそれほど重要なものではありません。しかし、テストコードを製造したり、それらを明瞭なものにしたりする場合にはとても重要なものとなります。仮にあなたがテストコードを読んでいるとしたとき、こういうコードよりも──

Calendar calendar = Calendar.getInstance();
calendar.setTimeZone(TimeZone.getTimeZone("Universal");
calendar.set(Calendar.DATE, 5);
calendar.set(Calendar.MONTH, 3 - 1);
calendar.set(Calendar.YEAR, 2004);
Date march5_2004 = calendar.getTime();

こういうコードや──

TimePoint march5_2003 = TimePoint.atMidnightGMT(2004, 03, 5);

あるいはこういうコードのほうが──

TimePoint march5_2003 = CalendarDate.date(2004, 03, 5);

ある特定の瞬間の時分秒ではなくて、まさに日付を表したいのだということがわかりやすいでしょう。日付を表現するのに時分秒を含むデータを用いなくてはならないのは、Java標準ライブラリのDateクラスを使用する場合に生じる典型的な制約です。(CalendarDateクラスについては後述します。)

日付や時間とは別に、業務アプリケーションの日時処理ロジックのなかでしばしば登場する2つの概念が持続〔時間〕(duration)と間隔(interval)です。持続は時間の長さです。奇妙なことに、Java標準ライブラリのクラス群を使用してこの概念を明示的に示す方法は存在しません。そういうわけで持続はしばしば整数値として保存されます。例えば──〔訳注:invoiceは請求書、grace periodは支払い猶予期間、delinquentは滞納状態を指す。〕

class Invoice {
 Date dueDate;
 int gracePeriodInDays;
 boolean isDelinquent(Date currentDate) {
  Calendar calendar = Calendar.getInstance();
  calendar.setTime(dueDate);
  calendar.add(Calendar.DATE, gracePeriodInDays);
  delinquencyDate = calendar.getTime();
  return currentDate.after(delinquencyDate);
 }
}

支払い猶予期間(grace period)変数はそれ単体で解釈することはできません。ただその名前を読み、それを操作するコードを見て、推測を行うことしかできません。Calendarクラスのaddメソッドはなんともぎこちないですし、当のCalendarオブジェクトの値そのものを書き換えてしまいます〔Calendarオブジェクトがミュータブルであることを問題にしている〕。TimeLanguageでは、次のようになります:

class Invoice {
 TimePoint due;
 Duration gracePeriod;
 boolean isDelinquent(TimePoint now) {
  return now.isAfter(due.plus(gracePeriod));
 }
}

dueDate.plus(gracePeriod)という式は、計算結果としてTimePointオブジェクトを返します。この式のなかで使用されているオブジェクトで、その処理中に変更されたものはありません。このため式を何度評価したところで結果は同じで、必ず決まった結果を返します。実際、Time and Moneyライブラリに登場する処理のほとんどは副作用なしの関数(side-effect-free functions)ですし、ほとんどのオブジェクトがイミュータブルです。

TimeLanguageにおけるもう1つの概念は間隔、つまり2つの時点で区切られた期間です。(実のところIntervalインターフェースは完全に汎用的なもので時間以外のものについても使用できるのですが、ここでは時間についてのみ議論することにしましょう。)TimeIntervalクラスは真偽値を返す式を表現するのに使用されます:

aTimeInterval.includes(aTimePoint)

間隔は2つの端点〔数学用語だがようするにここではTimePointオブジェクト〕か、1つの端点と持続〔Durationオブジェクト〕から定義することができます。次のような変数があったとして──

TimePoint march5_0hr = TimePoint.atMidnightGMT(2003, 3, 5);
TimePoint march7_0hr = TimePoint.atMidnightGMT(2003, 3, 7);
Duration twoDays = Duration.days(2); 

続く3つの式は、同じTimeIntervalオブジェクトを返します。(それらをequalsメソッドで比較するとtrueを返します。)

march5_0hr.until(march7_0hr)
twoDays.startingFrom(march5_0hr)
TimeInterval.over(march5_0hr, march7_0hr)

説明のため、架空の請求アプリがあるとしましょう。アプリはある特定の手数料が今回の請求に含まれるべきかどうかを判定しなくてはならないとします。これは次の式を評価することでチェックできます:

invoice.billingInterval().includes(charge.getWorkTime());

Invoiceオブジェクトが前述の振る舞いを実装する場合:

/* 1つめの実装 */
class Invoice {
 ...

 Date billingStart;
 Date billingEnd;

 boolean includeCharge(charge) {
   Date chargeDate =
       charge.getWorkTime()
   return chargeDate.after(billingStart) && chargeDate.before(billingEnd);
 }

}
/* 2つめの実装 */
class Invoice {
 ...

 TimeInterval billingPeriod;

 boolean includeCharge(charge) {
   return billingPeriod.includes(
      charge.getWorkTime());
 }

}

TimeLanguagバージョン(下側)のコードは簡潔であるだけでなく、エラーも少なくなります。左側のバージョンには、徹底的なユニットテストが必要になります。

以上の3つが、TimeLanguageにおける時間値の操作を理解するのに必要な概念です。しかしまだ他にもいくつかの概念とそれら概念により提示される可能性を具体化するための処理があります。これらについて理解するには、実装コードとテストコードを読み、JUnitで”example.*”パッケージ群のテストを実施してみるのが一番です。

CalendarDateクラス

その他の主要な区別立ては──それはJava標準ライブラリでも部分的に、かつぎこちないかたちで考慮されているものですが──それは時間(時分秒)とカレンダー日付の区別立てです。“2004年3月5日の午前0時”だとかその他のいかなる時間でもなく、まさに日付として2004年3月5日を表現したい、そういったことがあるでしょう。驚くべきことに、Java標準ライブラリにはこのようなことをする方法が用意されていません。先に見たTimePoint/TimeIntervalオブジェクトによるロジックに相応するものがCalendarDate/CalendarIntervalオブジェクトによるそれです。例えば:

CalendarDate march5_2003 = CalendarDate.from(2003, 3, 5);
CalendarDate march7_2003 = CalendarDate.from(2003, 3, 7);
Duration twoDays = Duration.days(2); 

これに続く3つの式はすべて同じCalendarIntervalオブジェクトを返します。(.equals()メソッドはtrueを返します。)

march5_2003.until(march7_2003)
twoDays.startingFrom(march5_2003) //later release
CalendarInterval.over(march5_2003, march7_2003)

しかしとくに2つのCarendarDateを対応させたときに重大なちがいが明らかになります。

march5_2003.until(march7_2003).asTimeInterval(aTimeZone)

まずタイムゾーンを指定する必要があります。それからCalendarDateはTimeIntervalに変換されます。TimePointではありません──これはある1日を指すもので間隔を指すものではありません。引き続く2つの式は同じ結果になります:

march5_2003.asTimeInterval(TimeZone.getTimeZone("Universal"))
march5_0hr.until(march6_0hr)

(・・・後略・・・)

* * *

(原典:“Time and Money Code Library”)