/* 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 import java.util.{Date=>JDate} import java.util.{Calendar => JCalendar} import java.util.{TimeZone => JZone} import java.util.Locale import java.text.SimpleDateFormat /* 2017-07-30("use (start~end):Bound[Date] instead", "v0.1.20") case class DateRange(start:Date,end:Date){ 2017-07-30("use Time(start~end) instead", "v0.1.20") lazy val time = Time(math.abs(end.ms - start.ms)*1E-3) override def toString = f"${time.nice}%8s from ${start.date} to ${end.date}" def <(t:Time):Boolean = time < t def >(t:Time):Boolean = time > t 2017-07-30("use (start~end).step(??) instead", "v0.1.20") def stepBy(step:Time)(func:Date=>Unit) = { @tailrec def next(d:Date):Unit = { func(d) val n = d + step if(n.ms <= end.ms) next(n) } next(start) } } */ object Date{ implicit object FormatDate extends Format[Date]{ override def apply(v:Date):String = v.isoLocal} implicit object ParsableDate extends Parsable[Date]{ def apply(v:String):Date = Date(v) } implicit object OrderingDate extends Ordering[Date]{ def compare(a:Date,b:Date):Int = { val dt = b.ms - a.ms if(dt > 0) -1 else if(dt < 0) 1 else 0 } } //--current time def s:Double = System.currentTimeMillis/1000.0 //new JDate().getTime //epoch: number of ms from ~1970 def ms:Long = System.currentTimeMillis //new JDate().getTime //epoch: number of ms from ~1970 def now:Date = fromMs(ms) //--construction def fromMs(d:Long):Date = new Date(new JDate(d)) //new JDate().getTime //epoch: number of ms from ~1970 def fromSec(d:Long):Date = fromMs(d*1000) //new JDate().getTime //epoch: number of seconds from ~1970 /*unix epoch moment in time*/ val epoch:Date = new Date(new JDate(0L)) //new JDate().getTime //epoch: number of seconds from ~1970 def apply():Date = new Date(new JDate()) def apply(d:Long):Date = new Date(new JDate(d)) //epoch: number of ms from ~1970 def apply(d:Double):Date = new Date(new JDate(d.toLong)) //epoch: number of ms from ~1970 //TODO add auto numeric info def krypton(str:String):Date = Krypton(str).date /*calendar based construction*/ private def apply(tz:JZone, y:Int, M:Int, d:Int, H:Int,m:Int,s:Int,ms:Int):Date ={ val jcal = JCalendar.getInstance(tz,Locale.US) //default timezone and locale lots of allocation jcal.set(y,M-1,d, H,m,s) jcal.set(JCalendar.MILLISECOND,ms) new Date(jcal.getTime) } //TODO wrap this with a custom zone wrapper class private val UTC:JZone = zone("UTC") //NO auto timezone or locals should be used private val Local:JZone = JZone.getDefault() //NO auto timezone or locals should be used private def zone(str:String):JZone = if(str=="local") Local else JZone.getTimeZone( str.toUpperCase match { case "EDT" => "GMT-04:00" case _ => str } ) private def jcal = JCalendar.getInstance(UTC,Locale.US) //default timezone and locale private def jcal(tz:String) = JCalendar.getInstance(zone(tz),Locale.US) def apply(y:Int, M:Int, d:Int, H:Int,m:Int,s:Int,ms:Int):Date = apply(UTC, y,M,d,H,m,s,ms) def apply(y:Int, M:Int, d:Int, H:Int,m:Int,s:Int):Date = apply(UTC, y,M,d,H,m,s,0) def apply(y:Int, M:Int, d:Int, H:Int,m:Int):Date = apply(UTC, y,M,d,H,m,0,0) def apply(y:Int, M:Int, d:Int, H:Int):Date = apply(UTC, y,M,d,H,0,0,0) def apply(y:Int, M:Int, d:Int):Date = apply(UTC, y,M,d,0,0,0,0) def apply(y:Int, M:Int):Date = apply(UTC, y,M,1,0,0,0,0) def apply(y:Int):Date = apply(UTC, y,1,1,0,0,0,0) def apply(tz:String, y:Int, M:Int, d:Int, H:Int,m:Int,s:Int,ms:Int):Date = apply(zone(tz), y,M,d,H,m,s,ms) def apply(tz:String, y:Int, M:Int, d:Int, H:Int,m:Int,s:Int):Date = apply(zone(tz), y,M,d,H,m,s,0) def apply(tz:String, y:Int, M:Int, d:Int, H:Int,m:Int):Date = apply(zone(tz), y,M,d,H,m,0,0) def apply(tz:String, y:Int, M:Int, d:Int, H:Int):Date = apply(zone(tz), y,M,d,H,0,0,0) def apply(tz:String, y:Int, M:Int, d:Int):Date = apply(zone(tz), y,M,d,0,0,0,0) def apply(tz:String, y:Int, M:Int):Date = apply(zone(tz), y,M,1,0,0,0,0) def apply(tz:String, y:Int):Date = apply(zone(tz), y,1,1,0,0,0,0) /**smart construction from multiple formats of date strings */ def apply(str:String):Date = { val s = str.trim if(isNum(s)){ val d = Try(s.toDouble).toOption getOrElse Double.NaN if (3E11 < d && d < 19E11) Date(d) else if(3E8 < d && d < 19E8) Date(d*1E3) // 3E8~20E8 --> 1979~2033 TODO add alternate epoch moduls ranges from 2022-04-27 else parseIso(s).get //1.979E9 1.979E11 } //TODO add krypton parse attempt else if(s startsWith "@") krypton(s) else Form.autoParse(s) getOrElse parseIso(s).get } //--day of the week sealed trait DayOfWeek{ def index:Int def name:String = this.toString def short:String = this.toString take 3 def isWeekend = index == 0 || index == 6 def isWeekday = !isWeekend def deltaTo(that:DayOfWeek):Int = { val x:Int = that.index - this.index if(x <= 6) x else x+7 } // def daysUntil(that:DayOfWeek):Int = { // val delta = this deltaTo that // if(delta < 0) delta + 7 else delta // } final def next:DayOfWeek = DayOfWeek.days( if(index < 6) index+1 else 0 ) final def prev:DayOfWeek = DayOfWeek.days( if(index > 0) index-1 else 6 ) } case object Sunday extends DayOfWeek{val index=0} case object Monday extends DayOfWeek{val index=1} case object Tuesday extends DayOfWeek{val index=2} case object Wednesday extends DayOfWeek{val index=3} case object Thursday extends DayOfWeek{val index=4} case object Friday extends DayOfWeek{val index=5} case object Saturday extends DayOfWeek{val index=6} object DayOfWeek{ val days = Array(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday) def parse(str:String):Option[DayOfWeek] = days.find{_.name.toLowerCase startsWith str.trim.toLowerCase} // require(index >=0 && index <= 6, "index of week must be [0,6] -> [Sunday,Saturday]") def fromIndex(index:Int):DayOfWeek = days(index) } private lazy val days = Seq("sun", "mon", "tue", "wed", "thu", "fri", "sat") private def isDay(str:String):Boolean = DayOfWeek.parse(str).isDefined //--months sealed trait Month{ def index:Int def name = this.toString def short = this.toString take 3 def numDays:Int } case object January extends Month{val index = 1; val numDays = 31} case object February extends Month{val index = 2; val numDays = 28} //sometimes 29 for leap case object March extends Month{val index = 3; val numDays = 31} case object April extends Month{val index = 4; val numDays = 30} case object May extends Month{val index = 5; val numDays = 31} case object June extends Month{val index = 6; val numDays = 30} case object July extends Month{val index = 7; val numDays = 31} case object August extends Month{val index = 8; val numDays = 31} case object September extends Month{val index = 9; val numDays = 30} case object October extends Month{val index =10; val numDays = 31} case object November extends Month{val index =11; val numDays = 30} case object December extends Month{val index =12; val numDays = 31} object Month{ val months = Array(January, February, March, April, May, June, July, August, September, October, November, December) def parse(str:String):Option[Month] = months.find{_.name.toLowerCase startsWith str.trim.toLowerCase} // require(index >=0 && index <= 6, "index of week must be [0,6] -> [Sunday,Saturday]") def fromIndex(index:Int):Month = months(index-1) } //--leaps def isLeapYear(y:Int):Boolean = (y %? 400) || ( (y %? 4) && !(y %? 100) ) // ever 4 years unless centennial unless 400 private def isNum(str:String):Boolean = str.forall{c => (c >= '0' && c <= '9') || c == '.' || c == 'E' || c =='e'} def parseIso(str:String):Try[Date] = { val prep = if(str.toUpperCase endsWith "Z") str.init + "+00" else str def isZoneChar(c:Char):Boolean = c.isLetter || c == '-' || c == '+' || c.isDigit val (t,z) = prep.foldLeft(("","")){ case ((t,""),c) if c.isDigit => (t+c, "") //is digit and no zone info obtained case ((t,z),c) if t.size > 9 && isZoneChar(c) => (t, z+c) //enough date found and a zone character was started case ((t,z),_) => (t, z) //strip everything else } //val (t,z) = s span {c => (c != '+') && (c != '-')} val formatString = ("yyyyMMddHHmmssSSSSSSSSSSS" take t.size) + (if(z=="") "" else if (z.head == '+' || z.head == '-') "X" else "z") val s = t+z Form(formatString) apply s } //this is an alias like method to make the format constructuors Time.format(dt) and Date.format(dt) similar def format(dt:Time):Format[Date] = Form.resolution(dt) class Form(val formatter:SimpleDateFormat) extends Format[Date]{ //parse and format def apply(s:String):Try[Date] = Try{Date(formatter parse s)} //parse override def apply(d:Date):String = formatter.format(d.date) //format //alternates def zone(tz:String):Form = Form(formatter.toPattern, tz) } object Form{ private def apply(fmt:String,tz:JZone=UTC, lc:Locale=Locale.US):Form = { val sf = new SimpleDateFormat(fmt,lc) sf setTimeZone tz new Form(sf) } def apply(fmt:String):Form = apply(fmt,UTC) //no auto timezone def apply(fmt:String,tz:String):Form = apply(fmt,zone(tz)) //no auto timezone //TODO use the same simple names for all types, i.e. Date.Form.year and Time.yr should not be different 1.s or Form.sec //SimpleDateFormat results are not thread safe, so ugh we need these to be defs even though they are an immutable concept def year = apply("yyyy") def month = apply("yyyy-MM") def day = apply("yyyy-MM-dd") def hour = apply("yyyy-MM-dd'T'HH'Z'") def minute = apply("yyyy-MM-dd'T'HH:mm'Z'") def sec = apply("yyyy-MM-dd'T'HH:mm:ss'Z'") def ms = apply("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") def compact = apply("yyyyMMdd'T'HHmmss.SSS'Z'") def local = apply("yyyy-MM-dd'T'HH:mm:ssX", Local) def http = apply("EEE, dd MMM yyyy HH:mm:ss z") def java = apply("EEE MMM dd HH:mm:ss Z yyyy", Local) def usMonthDay = apply("M/d") def usDay = apply("M/d/yyyy") def autoParse(s:String):Try[Date] = java(s) || http(s) //| local(s) | compact(s) | ms(s) | sec(s) | day(s) | month(s) | year(s) //TODO add the alternates back in // def tag = apply("yyyyMMdd'T'HHmmss'Z'" , "UTC") // def simple = apply("hh:mm z (EEE)") // def simpleDateTime = apply("yyyy MMMd h:mm:ssa z") // def prettyDateTime = apply("yyyy EEE MMMd h:mma z") // def prettyDate = apply("yyyy-MM-dd hhaa EEE z") // def resolution(dt:Time):Form = { if (dt >= 1.yr) Form.year else if(dt >= 1.yr/12) Form.month else if(dt >= 1.day) Form.day else if(dt >= 1.hr) Form.hour else if(dt >= 1.minute) Form.minute else if(dt >= 1.s) Form.sec else Form.ms } } object Calendar{ //where is the zone set here?? def apply(date:Date, tz:String="UTC"):Calendar = { val c = jcal(tz) //use this jcal based UTC first version instead of the default //JCalendar.getInstance() c.setTime(date.date) new Calendar(c) } } /**clean wrapper around java Calendar upper case indicates index from the beginning of the year or relevant range?*/ class Calendar(val cal:JCalendar) extends AnyVal{ def y = cal.get(JCalendar.YEAR) /**month index 1 = January*/ def M = cal.get(JCalendar.MONTH)+1 def month = Month.fromIndex(M) def d = cal.get(JCalendar.DAY_OF_MONTH) /**day of Year*/ def D = cal.get(JCalendar.DAY_OF_YEAR) /**day of week Calendar{SUNDAY:1 ... SATURDAY:7} , */ def w = cal.get(JCalendar.DAY_OF_WEEK) def dayOfWeek:DayOfWeek = DayOfWeek.days(w-1) /**week of year*/ def W = cal.get(JCalendar.WEEK_OF_YEAR) //def e = format("EEE") //def E = format("EEEE") def h = cal.get(JCalendar.HOUR) def H = cal.get(JCalendar.HOUR_OF_DAY) def m = cal.get(JCalendar.MINUTE) def s = cal.get(JCalendar.SECOND) def ms = cal.get(JCalendar.MILLISECOND) def z = cal.get(JCalendar.ZONE_OFFSET).ms //offset in ms //def Z = format("Z") def am = cal.get(JCalendar.AM_PM) == JCalendar.AM def pm = cal.get(JCalendar.AM_PM) == JCalendar.PM def ampm = if(am) "am" else "pm" } //--convenience methods val msPerDay:Long = 86400000L// 24L*60*60*1000 val sPerDay:Long = 86400L //24L*60*60 /**triangle wave from 0 to 1*/ def ramp(t:Time):Double = (ms.toDouble % t.ms)/t.ms } case class Date(val date:JDate) extends AnyVal with scala.math.Ordered[Date] { import Date.Form /* def simpleDateTime:String = Date.simpleDateTimeFormatter.format(date) def prettyDateTime:String = Date.prettyDateTimeFormatter.format(date) def simpleTime:String = Date.simpleTimeFormatter.format(date) def isoDateTime:String = Date.isoDateTimeFormatter.format(date) def isoDateTimeLocal:String = Date.isoDateTimeLocalFormatter.format(date) def isoDateTimeMs:String = Date.isoDateTimeMsFormatter.format(date) def isoDate:String = Date.isoDateFormatter.format(date) def isoTag:String = Date.isoTagFormatter.format(date) def iso:String = Date.isoDateTimeMsFormatter.format(date) def isoCompact:String = Date.isoCompactFormatter.format(date) def pretty:String = Date.prettyDateFormatter.format(date) */ 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 niceWork:String = { val d = cal d.dayOfWeek.short + " " + d.month.short + " " + ("%-2d" format d.d) } def usMonthDay = Form.usMonthDay apply this def usDay = Form.usDay 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"Date($iso)" def toRelativeString = {val dt = Date.now - this; dt.format(dt,0) + (if(dt > 0.s) " ago" else " from now")} def ~(that:Date):Bound[Date] = if(this < that) Bound(this,that) else Bound(that,this) def ~>(time:Time):Bound[Date] = Bound(this, this+time) def +-(time:Time):Bound[Date] = Bound(this - time/2, this + time/2) def +(time:Time):Date = Date(this.ms + time.ms) def -(time:Time):Date = Date(this.ms - time.ms) def -(that:Date):Time = Time((this.ms - that.ms)*1E-3) def compare(that:Date):Int = if(this.date before that.date) -1 else if(this.date after that.date) 1 else 0 //Date.Calendar(this) //TODO remove the following since the ordered trait gets these operators through `compare` // def <(that:Date):Boolean = this.date before that.date // def >(that:Date):Boolean = this.date after that.date 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: (Date.now - Date.unixEpoch).ms.toLong */ def ms:Long = date.getTime //ms since epoch /**Convenience function for the more direct: (Date.now - Date.unixEpoch).s.toLong */ def s:Long = date.getTime/1000 //seconds since epoch def ramp(t:Time):Double = ((Date.ms - ms).toDouble % t.ms)/t.ms def krypton:Krypton = Krypton(this) def floorDay:Date = Date(ms/Date.msPerDay*Date.msPerDay) /**round down to the nearest time common time beacon points*/ def round(dt:Time):Date = { 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) Date((c.y/100.0).round.toInt*100) //nearest century else if(dt0 >= 10.yr) Date((c.y/10.0).round.toInt*10) //nearest decade else if(dt0 >= 1.yr) Date((c.y + c.D/365.24).round.toInt) //nearest new year else if(dt0 >= 1.yr/12) Date(c.y, (c.M + c.d/30d).round.toInt) //nearest month else if(dt0 >= 1.day) Date(c.y, c.M, (c.d + c.H/24d).round.toInt) else if(dt0 >= 1.hr) Date(c.y, c.M, c.d, (c.H + c.m/60d).round.toInt) else if(dt0 >= 1.minute) Date(c.y, c.M, c.d, c.H, (c.m + c.s/60d).round.toInt) else if(dt0 >= 1.s) Date(c.y, c.M, c.d, c.H, c.m, (c.s + c.ms/1000d).round.toInt) else Date(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) } /**The planet of kyrpton provides concise specific date tagging and linear versioning*/ object Krypton{ object Span{ lazy val stage:Time = 1.day*36 // = 3 years lazy val month:Time = 1.day*36 // = 36 days exactly lazy val hour:Time = 1.day/36 // = 40.min exactly lazy val minute:Time = 1.day/(36**2) //~ 1.1.min lazy val s:Time = 1.day/(36**3) //~ 1.9s lazy val ms:Time = 1.day/(36**5) //~ 1.4.ms lazy val us:Time = 1.day/(36**7) //~ 1.1.us // lazy val ns:Time = 1.day/(36**9) //~ 1.1.us } def now:Krypton = Date.now.krypton def apply(kyrpton:String):Krypton = { val s = kyrpton filter {_.isLetterOrDigit} //Note: naive approach does not include leap seconds and relies on unix time... def padRight(num:String, size:Int) = num + "0"*(size-num.size) val daysSinceEpoch:Long = s take 3 base 36 //period-month-day val krMsIntoDay:Long = padRight(s drop 3,5) base 36 //hour-min-sec-flicker-ms Krypton(daysSinceEpoch.toInt, krMsIntoDay.toInt) } def apply(msEarthSinceEpoch:Long):Krypton = { val daysSinceEpoch = msEarthSinceEpoch/Date.msPerDay val kryptonMsIntoDay = msEarthSinceEpoch%Date.msPerDay*2187/3125 Krypton(daysSinceEpoch.toInt, kryptonMsIntoDay) } def apply(date:Date):Krypton = apply(date.ms) implicit object OrderingKrypton extends Ordering[Krypton]{ def compare(a:Krypton,b:Krypton):Int = { val dt = b.daysSinceEpoch - a.daysSinceEpoch if(dt > 0) -1 else if(dt < 0) 1 else { val dt = b.kryptonMsIntoDay - a.kryptonMsIntoDay if(dt > 0) -1 else if(dt < 0) 1 else 0 } } } } case class Krypton(daysSinceEpoch:Int, kryptonMsIntoDay:Long) extends scala.math.Ordered[Krypton]{ //note long is required for resolution of interest in the day lazy val krypton:String = daysSinceEpoch.base(36,3) + kryptonMsIntoDay.base(36,5) def stage:String = this take 1 // =3 Earth years def month:String = this take 2 // =36 Earth days def day:String = this take 3 // =1 Earth day def min:String = this take 5 // ~= 1.1 Earth min def s:String = this take 6 // ~= 1.9 Earth s def ms:String = this take 8 // ~= 1.4 Earth ms 2017-07-30("apply with an int usually means indexed charcter return, so use take instead","v0.2.16") def apply(res:Int):String = krypton take res def take(res:Int):String = krypton take res override def toString = krypton def compare(that:Krypton):Int = Krypton.OrderingKrypton.compare(this,that) //usEarth/usKrytpon => Ratio(24L*60*60*1000000, 36L**7) => Ratio(86400000000,78364164096) => Ratio(390625, 354294) //msEarth/msKrypton => Ratio(24L*60*60*1000, 36L**5) => Ratio(86400000,60466176) => Ratio(3125,2187) //s/kyptonS => Ratio(24L*60*60, 36L**3) => Ratio(50, 27) private def msEarthIntoDay:Long = kryptonMsIntoDay*3125/2187 private def msEarthSinceEpoch:Long = daysSinceEpoch*Date.msPerDay + msEarthIntoDay def date:Date = Date(msEarthSinceEpoch) //note |Version| contains considerable krypton features to get the next shortest uniq version }