Skip to content

Commit 175c62d

Browse files
committed
More date things.
1 parent 7bb25d3 commit 175c62d

File tree

5 files changed

+134
-18
lines changed

5 files changed

+134
-18
lines changed

doc/dates.md

+25
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,31 @@ E4|A1|3|2010-03-03T14:30:00+1100
291291
```
292292

293293

294+
Daylight Savings Time
295+
---------------------
296+
297+
Some extra treatment for daylight savings time.
298+
299+
Facts are stored with milliseconds since the start of the day. On
300+
daylight savings cross over days where an hour is _gained_ there is an
301+
overlap where multiple hour / minutes all map to the same "second of
302+
day" measure used by most date / time libraries (for example
303+
joda). Currently ivory handles this in the same way where "second of
304+
day" is computed only from the hour, minute and second - independent
305+
of DST related timezone offset changes.
306+
307+
To address this we could do one of two things:
308+
- annotate DST overlapped hours with an extra bit in the time field; or
309+
- offset time by an additional interval to handle the gained time.
310+
311+
However, both of these things require non-standard treament of "second
312+
of day" and will require code changes to ivory to handle.
313+
314+
To be clear, at this point ivory handles "second of day" based only on
315+
hour, minute and second of day. But in the future further information
316+
may be added to provide additional mechanisms to deal with DST
317+
overlaps.
318+
294319

295320
Current Status
296321
--------------

ivory-benchmark/src/main/scala/com/ambiata/ivory/benchmark/DatesBench.scala

+33-16
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,25 @@ case class DatesBench() extends SimpleScalaBenchmark {
5353
def time_hand_less_bad(n: Int) =
5454
hand_less_alloc(n, "201x-01-01")
5555

56+
def time_actual_ok(n: Int) =
57+
actual(n, "2012-01-01")
58+
59+
def time_actual_invalid(n: Int) =
60+
actual(n, "2012-99-01")
61+
62+
def time_actual_bad(n: Int) =
63+
actual(n, "201x-01-01")
64+
5665
/*
5766
* The basic joda approach, note: parseLocalDate throws exceptions so the catch is required.
5867
*/
5968
def joda(n: Int, s: String) = {
6069
val f = DateTimeFormat.forPattern("yyyy-MM-dd")
61-
repeat[String \/ Date](n) {
70+
repeat[Option[Date]](n) {
6271
try {
6372
val d = f.parseLocalDate(s)
64-
Date.unsafe(d.getYear.toShort, d.getMonthOfYear.toByte, d.getDayOfMonth.toByte).right[String]
65-
} catch { case e: Throwable => "bad".left }
73+
Date.unsafe(d.getYear.toShort, d.getMonthOfYear.toByte, d.getDayOfMonth.toByte).some
74+
} catch { case e: Throwable => None }
6675
}
6776
}
6877

@@ -71,12 +80,12 @@ case class DatesBench() extends SimpleScalaBenchmark {
7180
*/
7281
def regex(n: Int, s: String) = {
7382
val DateParser = """(\d\d\d\d)-(\d\d)-(\d\d)""".r
74-
repeat[String \/ Date](n) {
83+
repeat[Option[Date]](n) {
7584
s match {
7685
case DateParser(y, m, d) =>
77-
try Date.create(y.toShort, m.toByte, d.toByte).get.right
78-
catch { case e: Throwable => "bad".left }
79-
case _ => "bad".left
86+
try Date.create(y.toShort, m.toByte, d.toByte)
87+
catch { case e: Throwable => None }
88+
case _ => None
8089
}
8190
}
8291
}
@@ -85,26 +94,34 @@ case class DatesBench() extends SimpleScalaBenchmark {
8594
* A crude parser that unpacks things by hand.
8695
*/
8796
def hand(n: Int, s: String) =
88-
repeat[String \/ Date](n) {
97+
repeat[Option[Date]](n) {
8998
if (s.length != 10 || s.charAt(4) != '-' || s.charAt(7) != '-')
90-
"bad".left
99+
None
91100
else try
92-
Date.create(s.substring(0, 4).toShort, s.substring(5, 7).toByte, s.substring(9, 10).toByte).get.right
93-
catch { case e: Throwable => "bad".left }
101+
Date.create(s.substring(0, 4).toShort, s.substring(5, 7).toByte, s.substring(8, 10).toByte)
102+
catch { case e: Throwable => None }
94103
}
95104

96105
/*
97106
* A crude parser that unpacks things by hand with less allocation
98107
*/
99108
def hand_less_alloc(n: Int, s: String) =
100-
repeat[String \/ Date](n) {
109+
repeat[Option[Date]](n) {
101110
if (s.length != 10 || s.charAt(4) != '-' || s.charAt(7) != '-')
102-
"bad".left
111+
None
103112
else try {
104113
val y = s.substring(0, 4).toShort
105114
val m = s.substring(5, 7).toByte
106-
val d = s.substring(9, 10).toByte
107-
if (Date.isValid(y, m, d)) Date.unsafe(y, m, d).right else "bad".left
108-
} catch { case e: Throwable => "bad".left }
115+
val d = s.substring(8, 10).toByte
116+
if (Date.isValid(y, m, d)) Date.unsafe(y, m, d).some else None
117+
} catch { case e: Throwable => None }
118+
}
119+
120+
/*
121+
* A crude parser that unpacks things by hand with less allocation
122+
*/
123+
def actual(n: Int, s: String) =
124+
repeat[Option[Date]](n) {
125+
Dates.date(s)
109126
}
110127
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
package com.ambiata.ivory.core
2-
32
import scalaz._, Scalaz._
43
import org.joda.time.LocalDate
54
import com.ambiata.mundane.parse.ListParser
65

76
object Dates {
87
def parse(s: String): Date \/ DateTime =
98
???
9+
10+
// FIX It would be nice if we could avoid the joda overhead here, but we would need to
11+
// implement something to handle daylight savings offsets before we could.
12+
def datetime(s: String): Option[DateTime] =
13+
try {
14+
???
15+
// val d = DateTimeFormat.forPattern("yyyy-MM-ddTHH:mm:ss").parseLocalDateTime(s)
16+
//
17+
// DateTime.create(d.getYear.toShort, d.getMonthOfYear.toByte, d.getDayOfMonth.toByte, d.getMillisOfDay)
18+
} catch { case e: Throwable => None }
19+
20+
def date(s: String): Option[Date] =
21+
if (s.length != 10 || s.charAt(4) != '-' || s.charAt(7) != '-')
22+
None
23+
else try {
24+
val y = s.substring(0, 4).toShort
25+
val m = s.substring(5, 7).toByte
26+
val d = s.substring(8, 10).toByte
27+
if (Date.isValid(y, m, d)) Some(Date.unsafe(y, m, d)) else None
28+
} catch { case e: Throwable => None }
1029
}

ivory-core/src/main/scala/com/ambiata/ivory/core/Time.scala

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ object Time {
2525
def unsafe(seconds: Int): Time =
2626
new Time(seconds)
2727

28+
def isValid(seconds: Int): Boolean =
29+
seconds >= 0 && seconds < (60 * 60 * 24)
30+
2831
def create(seconds: Int): Option[Time] =
29-
(seconds >= 0 && seconds < (60 * 60 * 24)).option(unsafe(seconds))
32+
isValid(seconds).option(unsafe(seconds))
3033

3134
object Macros {
3235
import scala.reflect.macros.Context
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.ambiata.ivory.core
2+
3+
import com.ambiata.ivory.core.Arbitraries._
4+
import org.specs2._, matcher._, specification._
5+
import org.scalacheck._, Arbitrary._
6+
import org.joda.time._, format.DateTimeFormat
7+
import scalaz._, Scalaz._
8+
9+
class DatesSpec extends Specification with ScalaCheck { def is = s2"""
10+
11+
Date Parsing
12+
------------
13+
14+
Symmetric $symmetric
15+
Invalid year $year
16+
Invalid month $month
17+
Invalid day $day
18+
Edge cases $edge
19+
Exceptional - non numeric values $exceptional
20+
Round-trip with joda $joda
21+
Parses same as joda $jodaparse
22+
23+
24+
"""
25+
26+
def symmetric = prop((d: Date) =>
27+
Dates.date(d.hyphenated) must beSome(d))
28+
29+
def year = prop((d: Date) =>
30+
Dates.date("0100-%02d-%02d".format(d.month, d.day)) must beNone)
31+
32+
def month = prop((d: Date) =>
33+
Dates.date("%4d-13-%02d".format(d.year, d.day)) must beNone)
34+
35+
def day = prop((d: Date) =>
36+
Dates.date("%4d-%02d-32".format(d.year, d.month)) must beNone)
37+
38+
def exceptional = prop((d: Date) =>
39+
Dates.date(d.hyphenated.replaceAll("""\d""", "x")) must beNone)
40+
41+
def joda = prop((d: Date) =>
42+
Dates.date(new LocalDate(d.year, d.month, d.day).toString("yyyy-MM-dd")) must beSome(d))
43+
44+
def jodaparse = prop((d: Date) => {
45+
val j = DateTimeFormat.forPattern("yyyy-MM-dd").parseLocalDate(d.hyphenated)
46+
(j.getYear, j.getMonthOfYear, j.getDayOfMonth) must_== ((d.year.toInt, d.month.toInt, d.day.toInt)) })
47+
48+
def edge = {
49+
(Dates.date("2000-02-29") must beSome(Date(2000, 2, 29))) and
50+
(Dates.date("2001-02-29") must beNone)
51+
}
52+
}

0 commit comments

Comments
 (0)