
.13.e.source-code.date2.scala Maven / Gradle / Ivy
/*
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
//
// @deprecated("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)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy