Skip to content

Commit

Permalink
Merge pull request #8 from t2v/feature/java8-date-time
Browse files Browse the repository at this point in the history
Support Java8 Date&Time API
  • Loading branch information
gakuzzzz authored Jan 23, 2017
2 parents 0620ec2 + e1ce8cc commit 176f90f
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 155 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
language: scala

scala:
- 2.12.1

jdk:
- oraclejdk8

Expand Down
75 changes: 45 additions & 30 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,33 +1,23 @@
name := "holidays"

version := "4.0"

crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0")

scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature", "-language:implicitConversions")

libraryDependencies ++= Seq(
"com.github.nscala-time" %% "nscala-time" % "2.14.0",
"org.scalatest" %% "scalatest" % "3.0.1" % "test"
)

organization := "jp.t2v"

publishMavenStyle := true

publishTo := {
val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value)
Some("snapshots" at nexus + "content/repositories/snapshots")
else
Some("releases" at nexus + "service/local/staging/deploy/maven2")
}

publishArtifact in Test := false

pomIncludeRepository := { _ => false }

pomExtra := (
val commonSettings = Seq(
version := "5.0",
organization := "jp.t2v",
scalaVersion := "2.12.1",
crossScalaVersions := Seq("2.12.1"),
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature", "-language:implicitConversions"),
libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "3.0.1" % "test"
),
publishMavenStyle := true,
publishTo := {
val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value)
Some("snapshots" at nexus + "content/repositories/snapshots")
else
Some("releases" at nexus + "service/local/staging/deploy/maven2")
},
publishArtifact in Test := false,
pomIncludeRepository := { _ => false },
pomExtra :=
<url>https://github.com/t2v/holidays</url>
<licenses>
<license>
Expand All @@ -48,3 +38,28 @@ pomExtra := (
</developer>
</developers>
)

lazy val root = (project in file(".")).aggregate(core, joda).settings(
crossScalaVersions := Seq("2.12.1"),
publish := { },
publishArtifact := false,
packagedArtifacts := Map.empty
)

lazy val core = (project in file("core")).settings(commonSettings).settings(
name := "holidays",
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
)

lazy val joda = (project in file("joda")).settings(commonSettings).settings(
name := "holidays-joda",
libraryDependencies ++= Seq(
"joda-time" % "joda-time" % "2.9.7"
)
).dependsOn(core)




Original file line number Diff line number Diff line change
Expand Up @@ -21,66 +21,63 @@

package jp.t2v.util.locale

import com.github.nscala_time.time.Imports._
import org.joda.time.DateTimeConstants._
import java.time.{DayOfWeek, LocalDate}
import java.time.Month._
import java.time.DayOfWeek.{MONDAY, TUESDAY, WEDNESDAY}

import scala.annotation.switch
import scala.math.Ordering.Implicits._

object Holidays extends (LocalDate => Option[String]) {

private val 祝日法施行 = new LocalDate(1948, JULY, 20)
private val 明仁親王の結婚の儀 = new LocalDate(1959, APRIL, 10)
private val 昭和天皇大喪の礼 = new LocalDate(1989, FEBRUARY, 24)
private val 徳仁親王の結婚の儀 = new LocalDate(1993, JUNE, 9)
private val 即位礼正殿の儀 = new LocalDate(1990, NOVEMBER, 12)
private val 振替休日施行 = new LocalDate(1973, APRIL, 12)

private sealed abstract class DayOfWeek(val ordinal: Int)
private case object Monday extends DayOfWeek(1)
private case object Tuesday extends DayOfWeek(2)
private case object Wednesday extends DayOfWeek(3)
// private case object Thursday extends DayOfWeek(3)
// private case object Friday extends DayOfWeek(5)
// private case object Saturday extends DayOfWeek(6)
// private case object Sunday extends DayOfWeek(7)
private val 祝日法施行 = LocalDate.of(1948, JULY, 20)
private val 明仁親王の結婚の儀 = LocalDate.of(1959, APRIL, 10)
private val 昭和天皇大喪の礼 = LocalDate.of(1989, FEBRUARY, 24)
private val 徳仁親王の結婚の儀 = LocalDate.of(1993, JUNE, 9)
private val 即位礼正殿の儀 = LocalDate.of(1990, NOVEMBER, 12)
private val 振替休日施行 = LocalDate.of(1973, APRIL, 12)

private implicit class LocalDateWrapper(val d: LocalDate) extends AnyVal {
def is(w: DayOfWeek): Boolean = d.getDayOfWeek == w.ordinal
private[this] implicit class LocalDateWrapper(val d: LocalDate) extends AnyVal {
@inline def is(w: DayOfWeek): Boolean = d.getDayOfWeek == w
}

private class Condition[X](b: Boolean, s: => X) {
def |[Y >: X](n: => Y): Y = if (b) s else n
private[this] class Condition[X](b: Boolean, s: => X) {
def |[Y >: X](n: => Y): Y = if (b) s else n
}
private implicit class BooleanWrapper(val b: Boolean) extends AnyVal {
private[this] implicit class BooleanWrapper(val b: Boolean) extends AnyVal {
def ?[X](s: => X) = new Condition(b, s)
def opt[A](s: => A): Option[A] = if (b) Some(s) else None
@inline def opt(s: String): Option[String] = if (b) Some(s) else None
}

private def springEquinox(year: Int): Int = {
private[this] def springEquinox(year: Int): Int = {
if (year <= 1947) return 99
if (year <= 1979) return (20.8357 + (0.242194 * (year - 1980)) - ((year - 1983) / 4)).toInt
if (year <= 2099) return (20.8431 + (0.242194 * (year - 1980)) - ((year - 1980) / 4)).toInt
if (year <= 2150) return (21.851 + (0.242194 * (year - 1980)) - ((year - 1980) / 4)).toInt
99
}
private def autumnEquinox(year: Int): Int = {

private[this] def autumnEquinox(year: Int): Int = {
if (year <= 1947) return 99
if (year <= 1979) return (23.2588 + (0.242194 * (year - 1980)) - ((year - 1983) / 4)).toInt
if (year <= 2099) return (23.2488 + (0.242194 * (year - 1980)) - ((year - 1980) / 4)).toInt
if (year <= 2150) return (24.2488 + (0.242194 * (year - 1980)) - ((year - 1980) / 4)).toInt
99
}

private[this] implicit val localDateOrdering: Ordering[LocalDate] = Ordering.fromLessThan(_ isBefore _)

def apply(target: LocalDate): Option[String] = {
def holidayName(d: LocalDate): Option[String] = {
if (d < 祝日法施行) return None
val year = d.getYear
val month = d.getMonthOfYear
val month = d.getMonth
val day = d.getDayOfMonth
val weekOfMonth = (day - 1) / 7 + 1
(month: @switch) match {
month match {
case JANUARY =>
if (day == 1) Some("元日")
else if (year >= 2000) weekOfMonth == 2 && (d is Monday) opt "成人の日"
else if (year >= 2000) weekOfMonth == 2 && (d is MONDAY) opt "成人の日"
else day == 15 opt "成人の日"
case FEBRUARY =>
if (day == 11) year >= 1967 opt "建国記念の日"
Expand All @@ -98,15 +95,15 @@ object Holidays extends (LocalDate => Option[String]) {
case 3 => Some("憲法記念日")
case 4 =>
if (year >= 2007) Some("みどりの日")
else year >= 1986 && (d is Monday) opt "国民の休日"
else year >= 1986 && (d is MONDAY) opt "国民の休日"
case 5 => Some("こどもの日")
case 6 if year >= 2007 && ((d is Tuesday) || (d is Wednesday)) => Some("振替休日")
case 6 if year >= 2007 && ((d is TUESDAY) || (d is WEDNESDAY)) => Some("振替休日")
case _ => None
}
case JUNE =>
d == 徳仁親王の結婚の儀 opt "皇太子徳仁親王の結婚の儀"
case JULY =>
if (year >= 2003) (weekOfMonth == 3) && (d is Monday) opt "海の日"
if (year >= 2003) (weekOfMonth == 3) && (d is MONDAY) opt "海の日"
else (year >= 1996) && (day == 20) opt "海の日"
case AUGUST => day match {
case 11 => year >= 2016 opt "山の日"
Expand All @@ -117,13 +114,13 @@ object Holidays extends (LocalDate => Option[String]) {
if (day == equinox) {
Some("秋分の日")
} else if (year >= 2003) {
if (weekOfMonth == 3 && (d is Monday)) Some("敬老の日")
else (d is Tuesday) && day == (equinox - 1) opt "国民の休日"
if (weekOfMonth == 3 && (d is MONDAY)) Some("敬老の日")
else (d is TUESDAY) && day == (equinox - 1) opt "国民の休日"
} else {
year == 1966 && day == 15 opt "敬老の日"
}
case OCTOBER =>
if (year >= 2000) (weekOfMonth == 2) && (d is Monday) opt "体育の日"
if (year >= 2000) (weekOfMonth == 2) && (d is MONDAY) opt "体育の日"
else (year >= 1966) && (day == 10) opt "体育の日"
case NOVEMBER =>
if (day == 3) Some("文化の日")
Expand All @@ -134,14 +131,12 @@ object Holidays extends (LocalDate => Option[String]) {
}
}
def substitute(d: LocalDate): Option[String] = {
if ((d is Monday) && (d >= 振替休日施行) && holidayName(d - 1.day).isDefined) Some("振替休日")
if ((d is MONDAY) && (d >= 振替休日施行) && holidayName(d.minusDays(1)).isDefined) Some("振替休日")
else None
}
holidayName(target) orElse substitute(target)
}

def unapply(d: LocalDate): Option[String] = apply(d)

def unapply(d: DateTime): Option[String] = apply(d.toLocalDate)
def unapply[A: LocalDateConverter](value: A): Option[String] = apply(implicitly[LocalDateConverter[A]].apply(value))

}
10 changes: 10 additions & 0 deletions core/src/main/scala/jp/t2v/util/locale/Implicits.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package jp.t2v.util.locale

object Implicits {

implicit class HasHolidayNameOps[A](val a: A)(implicit ev: LocalDateConverter[A]) {
def holidayName: Option[String] = Holidays(ev(a))
def isHoliday: Boolean = holidayName.isDefined
}

}
44 changes: 44 additions & 0 deletions core/src/main/scala/jp/t2v/util/locale/LocalDateConverter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package jp.t2v.util.locale

import java.time.{LocalDate, LocalDateTime, ZonedDateTime}

@FunctionalInterface
abstract class LocalDateConverter[A] {
def apply(value: A): LocalDate
}
object LocalDateConverter extends LowPriorityLocalDateConverter {
implicit val localDate: LocalDateConverter[LocalDate] = d => d
implicit def localDateTime: LocalDateConverter[LocalDateTime] = _.toLocalDate
implicit def zonedDateTime: LocalDateConverter[ZonedDateTime] = _.toLocalDate
}

trait LowPriorityLocalDateConverter {
import scala.language.experimental.macros
implicit def jodaLocalDateConverter[A]: LocalDateConverter[A] = macro LocalDateConverterMacro.joda[A]
}

private[locale] object LocalDateConverterMacro {
import scala.reflect.macros.blackbox

def joda[A: c.WeakTypeTag](c: blackbox.Context): c.Expr[LocalDateConverter[A]] = {
import c.universe._
val A = weakTypeTag[A].tpe
val expr = A.toString match {
case "org.joda.time.LocalDate" =>
q"""new jp.t2v.util.locale.LocalDateConverter[$A] {
def apply(d: $A) = java.time.LocalDate.of(d.getYear, d.getMonthOfYear, d.getDayOfMonth)
}"""
case "org.joda.time.DateTime" =>
q"""new jp.t2v.util.locale.LocalDateConverter[$A] {
def apply(value: $A) = {
val d = value.toLocalDate
java.time.LocalDate.of(d.getYear, d.getMonthOfYear, d.getDayOfMonth)
}
}"""
case _ =>
c.abort(c.enclosingPosition, s"Implicit LocalDateConverter[$A] is missing.")
}
c.Expr[LocalDateConverter[A]](expr)
}

}
73 changes: 73 additions & 0 deletions core/src/test/scala/jp/t2v/util/locale/HolidaysSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package jp.t2v.util.locale

import java.time.LocalDate
import java.time.format.DateTimeFormatter

import org.scalatest._

class HolidaysSpec extends FlatSpec with Matchers {

"The Holidays" should "return holiday name" in {
import jp.t2v.util.locale.Implicits._

LocalDate.of(2012, 1, 1).holidayName should equal (Some("元日"))
LocalDate.of(2012, 1, 2).holidayName should equal (Some("振替休日"))
LocalDate.of(2012, 1, 9).holidayName should equal (Some("成人の日"))
LocalDate.of(2012, 2, 11).holidayName should equal (Some("建国記念の日"))
LocalDate.of(2012, 3, 20).holidayName should equal (Some("春分の日"))
LocalDate.of(2012, 4, 29).holidayName should equal (Some("昭和の日"))
LocalDate.of(2012, 4, 30).holidayName should equal (Some("振替休日"))
LocalDate.of(2012, 5, 3).holidayName should equal (Some("憲法記念日"))
LocalDate.of(2012, 5, 4).holidayName should equal (Some("みどりの日"))
LocalDate.of(2012, 5, 5).holidayName should equal (Some("こどもの日"))
LocalDate.of(2012, 7, 16).holidayName should equal (Some("海の日"))
LocalDate.of(2012, 9, 17).holidayName should equal (Some("敬老の日"))
LocalDate.of(2012, 9, 22).holidayName should equal (Some("秋分の日"))
LocalDate.of(2012, 10, 8).holidayName should equal (Some("体育の日"))
LocalDate.of(2012, 11, 3).holidayName should equal (Some("文化の日"))
LocalDate.of(2012, 11, 23).holidayName should equal (Some("勤労感謝の日"))
LocalDate.of(2012, 12, 23).holidayName should equal (Some("天皇誕生日"))
LocalDate.of(2012, 12, 24).holidayName should equal (Some("振替休日"))

// 水曜日の振替休日
LocalDate.of(2009, 5, 6).holidayName should equal (Some("振替休日"))
}

implicit class WrapString(s: String) {
def ld: LocalDate = DateTimeFormatter.ofPattern("yyyy/MM/dd").parse(s, LocalDate.from)
}

"Holidays extractor" should "extract holiday name" in {
val d = Seq(
"2012/04/28".ld,
"2012/04/29".ld,
"2012/04/30".ld,
"2012/05/01".ld,
"2012/05/02".ld,
"2012/05/03".ld,
"2012/05/04".ld,
"2012/05/05".ld,
"2012/05/06".ld
)

val actual = d map {
case Holidays(name) => name
case _ => "平日"
}

val expected = Seq(
"平日",
"昭和の日",
"振替休日",
"平日",
"平日",
"憲法記念日",
"みどりの日",
"こどもの日",
"平日"
)

actual should equal (expected)
}

}
Loading

0 comments on commit 176f90f

Please sign in to comment.