JodaTimeというJavaのライブラリがある。しかし、もちろんJavaなので、メソッド名がplusYearsとかになっているわけだが、それをラッピングしちゃって、
today + "3years" + "1month" - "2week" + "2days"
みたいな構文で呼べるようにするのが最終的な目標。ちなみに、実用とかを考えるより、scalaの勉強のためというのが大きいので、効率とかはあまり考えないことにした。
とりあえず出来る限りscalaっぽく、書いてみるのと、でも簡潔に、でも型安全に、でも読みやすくとかを重視。
で、できたのが以下のコード
object ScalaJoda{
/** メソッドの引数として有効なパターン */
private val ParamRegex = {"""(-?[0-9]+)(""" + Field.names.mkString("|") + ")(s?)"}.r/** DateTimeからの暗黙変換 */
implicit def toScalaJoda(time:DateTime) = new ScalaJoda(time)
/** 抽出子 */
private object Value{
/**
* @param s もととなる足したい数値+field名の形式のString
* @return 形式があってたら、数値と、fieldに分解したものを返す
*/
def unapply(s:String):Option[(Int,Field )] = s match{
case ParamRegex(number,field,_) => Some( number.toInt , Field(field) )
case _ => None
}
}
/** fieldの種類を表す */
private object Field{/** コンパニオンクラスのインスタンスをSetの中に保存(これ以外にインスタンスはない) */
private val fieldSet =
Set(
Field("year" ,_.plusYears(_) , _.minusYears(_) ),
Field("month" ,_.plusMonths(_) , _.minusMonths(_) ),
Field("day" ,_.plusDays(_) , _.minusDays(_) ),
Field("hour" ,_.plusHours(_) , _.minusHours(_) ),
Field("week" ,_.plusWeeks(_) , _.minusWeeks(_) ),
Field("minute" ,_.plusMinutes(_) , _.minusMinutes(_) ),
Field("second" ,_.plusSeconds(_) , _.minusSeconds(_) ),
Field("milli" ,_.plusMillis(_) , _.minusMillis(_) )
)
val names = fieldSet.map( _.name )
/** Stringのコンストラクタっぽく使えるが、newするのは無駄なので、fieldSetから探して返す */
private[ScalaJoda] def apply(s:String):Field = {
val Some(result) = fieldSet.find{ s == _.name }
result
}
}
/** まともに書くと名前長いので、別名つけた */
private type FuncT = Function2[DateTime,Int,DateTime]
final private case class Field private(
name:String, plusFunc:FuncT, minusFunc:FuncT
){
def getFunc(t:FuncType.Value):FuncT = t match {
case FuncType.plus => plusFunc
case FuncType.minus => minusFunc
}
}
/** getFuncの引数で使う */
final private object FuncType extends Enumeration{
val plus,minus = Value
}
}/**
* @param self 内部に保持するDataTime
これをラップして、違うかたちで関数を提供
*/
class ScalaJoda(private val self:DateTime){
import ScalaJoda._
/** +と-のメソッドのためのヘルパー関数 */
private def sub( funT:FuncType.Value , strSeq:String* ):DateTime = {val paramfields =
for(s <- strSeq)yield{//それぞれの引数をチェックし、かつ、数字と適用する関数を抽出
s match{
case Value(num,f) => (num,f.getFunc(funT))
case _ => throw new IllegalArgumentException( s +" は入力形式として正しくありません" )
}
}
//順に関数適用
paramfields.foldLeft(self){
case ( receiver , ( param, method) ) => method( receiver , param )
}
}
/**
* 引数で渡された文字列を、それぞれ数値とフィールドに分解して適用する
* @param strSeqScalaDoja.pattern
にマッチする文字列(の可変長引数)
* @return 新しいDateTime
オブジェクト
*/
def + (strSeq:String*):DateTime = sub( FuncType.plus , strSeq: _* )/** +のほうと使い方同じ */
def - (strSeq:String*):DateTime = sub( FuncType.minus, strSeq: _* )
}
ところで、関数オブジェクトだと以下の14文字だけで、
_.plusYears(_)
classファイルが一個できるから、classファイルの数がすごいことになった(笑)
まあ一番のポイントというか基本的なところは、DateTimeを暗黙変換して、計算して、またDateTimeを返すようにすれば、他に影響なく、単にDateTimeのメソッドが増えた様な感じで、手軽に使えるという点かなぁ。
(DateTimeがfinalなので継承して拡張することができないが、あたかも継承して拡張したように使える。)
とりあえず、重複の定義を避けるために、+と-で同じヘルパー関数呼ぶようにしたり、plusYearsとかplusMonthsとか関数の形式が同じなので、一度関数オブジェクトとしてSetに保存してみた。(やりすぎか?)
Javaでやったら、絶対こんな形にはできないよねぇ。ていうかJavaと比べてもあんまり意味ないよね。
あと最初は単一の引数をとる関数作ったんだけど、可変長とるやつだけ定義すればよくね?ってことで、
val today = new JodaTime
today + ( "3years" ,"1month" ,"2week" , "-2days" )
みたいな感じでも呼べるようにしてみた。