/* Copyright 2010 Aaron J. Radke Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cc.drx //http://howardhinnant.github.io/date_algorithms.html MIT and public domain attributions from the summary section of the paper object Date2{ def ms:Long = System.currentTimeMillis //new JDate().getTime //epoch: number of ms from ~1970 //--construction def fromMs(d:Long):Date2 = new Date2(d) def fromSec(d:Long):Date2 = fromMs(d*1000) //new JDate().getTime //epoch: number of seconds from ~1970 /*unix epoch moment in time*/ def now:Date2 = fromMs(ms) val epoch:Date2 = new Date2(0L) //new JDate().getTime //epoch: number of seconds from ~1970 //-- epoch construction // def apply(d:Long):Date2 = new Date2(d) //epoch: number of ms from ~1970 def apply(d:Double):Date2 = new Date2(d.toLong) //epoch: number of ms from ~1970 //TODO add auto numeric info //-- civil constructions def apply(year:Int):Date2 = Date2(year,1,1) def apply(year:Int, month:Int):Date2 = Date2(year,month,1) def apply(year:Int, month:Int, day:Int):Date2 = DateCivil(year,month,day).date //-- date and time constructions def apply(year:Int, month:Int, day:Int, hour:Int):Date2 = Date2(year,month,day,hour,0,0,0) def apply(year:Int, month:Int, day:Int, hour:Int, min:Int):Date2 = Date2(year,month,day,hour,min,0,0) def apply(year:Int, month:Int, day:Int, hour:Int, min:Int, sec:Int):Date2 = Date2(year,month,day,hour,min,sec) def apply(year:Int, month:Int, day:Int, hour:Int, min:Int, sec:Int, ms:Int):Date2 = Date2( DateCivil(year,month,day).epochDay*msPerDay + DateTime24(hour,min,sec,ms).msInDay ) //-- day of week constructions /**nth dayOfWeek of a month*/ def apply(year:Int, month:Int, day:Date.DayOfWeek,n:Int):Date2 = DateCivil(year,month,1).date.get(day,n) /**nth dayOfWeek of the year*/ def apply(year:Int, day:Date.DayOfWeek,n:Int):Date2 = DateCivil(year,1,1).date.get(day,n) /**first dayOfWeek of a month*/ def apply(year:Int, month:Int, day:Date.DayOfWeek):Date2 = DateCivil(year,month,1).date.get(day,1) /**first dayOfWeek of a year*/ def apply(year:Int, day:Date.DayOfWeek):Date2 = DateCivil(year,1,1).date.get(day,1) //--constants final val msPerDay = 86400000L // ms/day => 24*3600*1000 //--type classes implicit object FormatDate2 extends Format[Date2]{ override def apply(v:Date2):String = v.iso} //TODO implicit object ParsableDate2 extends Parsable[Date2]{ def apply(v:String):Date2 = Date2(v) } implicit object OrderingDate2 extends Ordering[Date2]{ def compare(a:Date2,b:Date2):Int = a.ms compare b.ms } implicit object BoundableDate2 extends Bound.Boundable[Date2]{ def lessThan(a:Date2,b:Date2) = a.ms < b.ms def interpolate(min:Date2,max:Date2, ratio:Double) = new Date2( (min.ms + ratio*(max.ms - min.ms)).toLong) def ratioOf(min:Date2,max:Date2, x:Date2) = (x.ms - min.ms).toDouble/(max.ms - min.ms) override def dist(min:Date2,max:Date2):Double = (max.ms - min.ms).toDouble.abs override def gain(min:Date2,max:Date2):Double = (max.ms/min.ms).toDouble.abs override def toString(min:Date2,max:Date2) = f"Bound(${Time(dist(min,max)*1E-3).format}%8s from ${min.iso} to ${max.iso})" } implicit object BoundDate2Step extends Bound.BoundStep[Date2,Time]{ def next(a:Date2, step:Time):Date2 = a + step } } object DateCivil { /** construct from number of days from 1970-01-01 * http://howardhinnant.github.io/date_algorithms.html * */ def apply(epochDay:Int):DateCivil = { val z:Int = epochDay + 719468 //epoch shift to March1st (the day after leap year day) val era:Int = (if(z >= 0) z else z - 146096) / 146097 // era val doe:Int = z - era * 146097 //day of epoch [0, 146096] val yoe:Int = (doe - doe/1460 + doe/36524 - doe/146096) / 365 // [0, 399] val doy:Int = doe - (365*yoe + yoe/4 - yoe/100) // [0, 365] val mp:Int = (5*doy + 2)/153 // [0, 11] //--month val month = mp + (if(mp < 10) 3 else -9) // [1, 12] //--year val year = yoe + era * 400 + (if(month <= 2) 1 else 0) //--day val day = doy - (153*mp+2)/5 + 1; // [1, 31] DateCivil(year, month, day) } private val commonMonthDays = Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) def isLeapYear(year:Int):Boolean = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) def lastDayOfMonth(year:Int, month:Int):Int = if(month != 2) commonMonthDays(month-1) else if(isLeapYear(year)) 28 else 29 // /**civil date extractor from a Date2 also instead of just a DateCivil*/ def unapply(date:Date2):Option[(Int,Int,Int)] = { val civil = date.civil Option( (civil.y, civil.m, civil.d) ) } } /** year from Christ:0, month:[1,12], day:[1,31] */ case class DateCivil(y:Int, m:Int, d:Int) { //TODO this is where the time zone is required final def date = Date2(epochDay*Date2.msPerDay) final def epochDay:Int = { //--shift year and month with month offset for february 29 as last day of `yp` val yp = if(m > 2) y else y-1 val mp = if(m > 2) m-3 else m+9 //--error and calculations val era = (if(yp >= 0) yp else yp - 399) / 400 //leap year patterns are repeat every 400 years val yoe = yp - era*400 //year of epoch [0,399] val doy = (153*mp + 2)/5 + d-1 //day of year [0, 365] val doe = yoe*365 + yoe/4 - yoe/100 + doy //day of epoch [0, 146096] era*146097 + doe - 719468 } def isoYear:String = f"$y%04d" def isoMonth:String = f"$y%04d-$m%02d" def isoDay:String = f"$y%04d-$m%02d-$d%02d" def iso:String = isoDay def month:Date.Month = Date.Month.fromIndex(m) } /**Time of day in hour:min:sec*/ object DateTime24{ val msPerHour:Long = 60*60*1000L val msPerMin:Int = 60*1000 val msPerSec:Int = 1000 def apply(msInDay:Long):DateTime24 = { val (h, msInHour) = msInDay divMod msPerHour val (m, msInMin) = msInHour divMod msPerMin val (s, ms) = msInMin divMod msPerSec DateTime24(h.toInt, m.toInt, s.toInt, ms.toInt) } } case class DateTime24(hour:Int, min:Int, sec:Int, ms:Int) { import DateTime24._ def isoMin:String = f"$hour%02d:$min%02d" def isoSec:String = f"$hour%02d:$min%02d:$sec%02d" def isoMs:String = f"$hour%02d:$min%02d:$sec%02d.$ms%03d" def msInDay:Long = hour*msPerHour + min*msPerMin + sec*msPerSec + ms } case class Date2(val ms:Long) extends AnyVal with scala.math.Ordered[Date2] { import Date.DayOfWeek import Date2._ import DateTime24._ private def epochDay:Int = (ms/Date2.msPerDay).toInt private def msInDay:Long = ms % Date2.msPerDay //TODO FIXME add time zone dependency def civil:DateCivil = DateCivil(epochDay) def y = civil.y def d = civil.d //halfway lazy val for civil def m = civil.m def month = civil.month private def time:DateTime24 = DateTime24(msInDay) // def h = time.hour // def min = time.min // def s = time.sec /** day of week 0 to 6 -> [Sun, Sat] */ def dowIndex:Int = { val z = epochDay; if(z >= -4) (z+4) % 7 else (z+5) % 7 + 6 } def dayOfWeek:DayOfWeek = DayOfWeek.fromIndex(dowIndex) def dow:DayOfWeek = dayOfWeek /** get the n'th (1 = first) day of week from a given date (current day is the first)*/ def get(that:DayOfWeek,n:Int=1):Date2 = { val d = this.dow deltaTo that val daysTo = { val weekShift = (n.abs-1)*7 if(n > 0) (if(d < 0) d + 7 else d) + weekShift else (if(d > 0) d - 7 else d) - weekShift } Date2(ms + daysTo*Date2.msPerDay) } /** get the NEXT n'th (1 = first) day of week from a given date (current day is not counted)*/ def next(that:DayOfWeek,n:Int=1):Date2 = { val nOffset = if(this.dow == that) 1 else 0 get(that, n.sgn*(n.abs + nOffset) ) } /** get the Previous n'th (1 = first) day of week from a given date (current day is not counted)*/ def prev(that:DayOfWeek,n:Int=1):Date2 = next(that, -n) //--ISO 8601 formats https://en.wikipedia.org/wiki/ISO_8601 // def zoneOffset:Time = Time(0) //TODO make this configurable (maybe an implicit ???, but it shoudl be explicit) // def isoZone = if(zoneOffset==Time(0)) "Z" else { // val (h,m) = zoneOffset.min.round.toInt divMod 60 // f"$h%+02d:$m%02d" // } def isoZone = "Z" def isoYear:String = civil.isoYear def isoDay:String = civil.isoDay def isoMin:String = civil.isoDay + "T" + time.isoMin + isoZone def isoSec:String = civil.isoDay + "T" + time.isoSec + isoZone def isoMs:String = civil.isoDay + "T" + time.isoMs + isoZone def iso:String = isoSec override def toString = s"Date2($iso)" // def isoYr:String = Form.year apply this // def isoDay:String = Form.day apply this // def iso:String = Form.sec apply this // def isoHour:String = Form.hour apply this // def isoMinute:String = Form.minute apply this // def isoMs:String = Form.ms apply this // def isoCompact:String = Form.compact apply this // def isoLocal:String = Form.local apply this // def rfcJava:String = Form.java apply this // def rfcHttp:String = Form.http apply this // // def %(fmt:String):String = this format fmt // private def lowerAMPM(s:String) = s.replace("AM","am").replace("PM","pm") // def format(fmt:String):String = Form(fmt) apply this // def format(fmt:String,tz:String):String = Form(fmt,tz) apply this // def format(f:Form):String = f apply this // // def format(formatter:SimpleDateFormat):String = formatter.format(date).replace("AM","am").replace("PM","pm") // // // //def zone = ??? //TODO why isn't a zone associated with a time already? // // override def toString = s"Date2($iso)" // def toRelativeString = {val dt = Date2.now - this; dt.format(dt,0) + (if(dt > 0.s) " ago" else " from now")} // def ~(that:Date2):Bound[Date2] = if(this < that) Bound(this,that) else Bound(that,this) def ~>(time:Time):Bound[Date2] = Bound(this, this+time) def +-(time:Time):Bound[Date2] = Bound(this - time/2, this + time/2) def +(time:Time):Date2 = Date2(this.ms + time.ms) def -(time:Time):Date2 = Date2(this.ms - time.ms) def -(that:Date2):Time = Time((this.ms - that.ms)*1E-3) // def compare(that:Date2):Int = if(this.ms < that.ms) -1 else if(this.ms > that.ms) 1 else 0 // // def cal = Date.Calendar(this) // def cal(tz:String) = Date.Calendar(this,tz) // def dayOfWeek:Date.DayOfWeek = cal.dayOfWeek // // 2017-07-30("use ms instead (epoch is a point in time 1970 if epoch is time since epoch it can confusingly then be ms or seconds)","v0.2.13") // def epoch:Long = ms //ms since epoch // // /**Convenience function for the more direct: (Date2.now - Date2.unixEpoch).ms.toLong */ // def ms:Long = date.getTime //ms since epoch // /**Convenience function for the more direct: (Date2.now - Date2.unixEpoch).s.toLong */ // def s:Long = date.getTime/1000 //seconds since epoch // // def ramp(t:Time):Double = ((Date2.ms - ms).toDouble % t.ms)/t.ms // // def krypton:Krypton = Krypton(this) // def roundDay:Date2 = Date2(ms/msPerDay*msPerDay) def roundHour:Date2 = Date2(ms/msPerHour*msPerHour) def roundMin:Date2 = Date2(ms/msPerMin*msPerMin) def roundSec:Date2 = Date2(ms/msPerSec*msPerSec) // /**round down to the nearest time common time beacon points*/ // def round(dt:Time):Date2 = { // val dt0 = dt.abs //make sure delta is an absolute value of time // val c = this.cal("UTC") //cal is a def so make it stable here and smaller for reference... // if (dt0 >= 100.yr) Date2((c.y/100.0).round.toInt*100) //nearest century // else if(dt0 >= 10.yr) Date2((c.y/10.0).round.toInt*10) //nearest decade // else if(dt0 >= 1.yr) Date2((c.y + c.D/365.24).round.toInt) //nearest new year // else if(dt0 >= 1.yr/12) Date2(c.y, (c.M + c.d/30d).round.toInt) //nearest month // else if(dt0 >= 1.day) Date2(c.y, c.M, (c.d + c.H/24d).round.toInt) // else if(dt0 >= 1.hr) Date2(c.y, c.M, c.d, (c.H + c.m/60d).round.toInt) // else if(dt0 >= 1.minute) Date2(c.y, c.M, c.d, c.H, (c.m + c.s/60d).round.toInt) // else if(dt0 >= 1.s) Date2(c.y, c.M, c.d, c.H, c.m, (c.s + c.ms/1000d).round.toInt) // else Date2(c.y, c.M, c.d, c.H, c.m, c.s, c.ms) // } // /**format a date at delta time resolution*/ // def format(dt:Time):String = Date.Form.resolution(dt.abs)(this) }