aboutsummaryrefslogtreecommitdiff
path: root/core-types/src/main/scala/xyz/driver/core/date.scala
blob: 54540939105e41b9eb02e030499bca854576409a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package xyz.driver.core

import java.util.Calendar

import enumeratum._
import scalaz.std.anyVal._
import scalaz.syntax.equal._

import scala.collection.immutable.IndexedSeq
import scala.util.Try

/**
  * Driver Date type and related validators/extractors.
  * Day, Month, and Year extractors are from ISO 8601 strings => driver...Date integers.
  * TODO: Decouple extractors from ISO 8601, as we might want to parse other formats.
  */
object date {

  sealed trait DayOfWeek extends EnumEntry
  object DayOfWeek extends Enum[DayOfWeek] {
    case object Monday    extends DayOfWeek
    case object Tuesday   extends DayOfWeek
    case object Wednesday extends DayOfWeek
    case object Thursday  extends DayOfWeek
    case object Friday    extends DayOfWeek
    case object Saturday  extends DayOfWeek
    case object Sunday    extends DayOfWeek

    val values: IndexedSeq[DayOfWeek] = findValues

    val All: Set[DayOfWeek] = values.toSet

    def fromString(day: String): Option[DayOfWeek] = withNameInsensitiveOption(day)
  }

  type Day = Int @@ Day.type

  object Day {
    def apply(value: Int): Day = {
      require(1 to 31 contains value, "Day must be in range 1 <= value <= 31")
      value.asInstanceOf[Day]
    }

    def unapply(dayString: String): Option[Int] = {
      require(dayString.length === 2, s"ISO 8601 day string, DD, must have length 2: $dayString")
      Try(dayString.toInt).toOption.map(apply)
    }
  }

  type Month = Int @@ Month.type

  object Month {
    def apply(value: Int): Month = {
      require(0 to 11 contains value, "Month is zero-indexed: 0 <= value <= 11")
      value.asInstanceOf[Month]
    }
    val JANUARY   = Month(Calendar.JANUARY)
    val FEBRUARY  = Month(Calendar.FEBRUARY)
    val MARCH     = Month(Calendar.MARCH)
    val APRIL     = Month(Calendar.APRIL)
    val MAY       = Month(Calendar.MAY)
    val JUNE      = Month(Calendar.JUNE)
    val JULY      = Month(Calendar.JULY)
    val AUGUST    = Month(Calendar.AUGUST)
    val SEPTEMBER = Month(Calendar.SEPTEMBER)
    val OCTOBER   = Month(Calendar.OCTOBER)
    val NOVEMBER  = Month(Calendar.NOVEMBER)
    val DECEMBER  = Month(Calendar.DECEMBER)

    def unapply(monthString: String): Option[Month] = {
      require(monthString.length === 2, s"ISO 8601 month string, MM, must have length 2: $monthString")
      Try(monthString.toInt).toOption.map(isoM => apply(isoM - 1))
    }
  }

  type Year = Int @@ Year.type

  object Year {
    def apply(value: Int): Year = value.asInstanceOf[Year]

    def unapply(yearString: String): Option[Int] = {
      require(yearString.length === 4, s"ISO 8601 year string, YYYY, must have length 4: $yearString")
      Try(yearString.toInt).toOption.map(apply)
    }
  }

  final case class Date(year: Int, month: Month, day: Int) {
    override def toString = f"$year%04d-${month + 1}%02d-$day%02d"
  }

  object Date {
    implicit def dateOrdering: Ordering[Date] = Ordering.fromLessThan { (date1, date2) =>
      if (date1.year != date2.year) {
        date1.year < date2.year
      } else if (date1.month != date2.month) {
        date1.month < date2.month
      } else {
        date1.day < date2.day
      }
    }

    def fromString(dateString: String): Option[Date] = {
      dateString.split('-') match {
        case Array(Year(year), Month(month), Day(day)) => Some(Date(year, month, day))
        case _                                         => None
      }
    }
  }
}