diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92322c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +target/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..28174f8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +jdk: + - oraclejdk8 +language: scala +script: "sbt publish" diff --git a/README.md b/README.md new file mode 100644 index 0000000..f35b587 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# jewish-date + +#### A Scala library for converting between the Gregorian calendar and the Jewish calendar, cross-compiled for JVM and JS + +Heavily based on and derived from https://github.com/KosherJava/zmanim, but taking advantage of the Java 8 `java.time` API, and as a Scala-idiomatic API. + +### Features + +* Convert from `java.time.LocalDate` to `jewishdate.JewishDate` and vice versa +* Get date of Yomim Tovim and Yom Tov of a date (currently Diaspora only; pull requests welcome) diff --git a/bintray.sbt b/bintray.sbt new file mode 100644 index 0000000..db01307 --- /dev/null +++ b/bintray.sbt @@ -0,0 +1,12 @@ +publishMavenStyle in ThisBuild := true + +publishTo in ThisBuild := Some("Project Bintray" at "https://api.bintray.com/maven/naftoligug/maven/jewish-date") + +sys.env.get("BINTRAYKEY").toSeq.map { key => + credentials in ThisBuild += Credentials( + "Bintray API Realm", + "api.bintray.com", + "naftoligug", + key + ) +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..86698da --- /dev/null +++ b/build.sbt @@ -0,0 +1,25 @@ +ThisBuild / organization := "io.github.nafg" +ThisBuild / scalaVersion := "2.12.4" +ThisBuild / scalacOptions ++= Seq("-deprecation", "-unchecked", "-feature") + +lazy val jewishDate = + crossProject.crossType(CrossType.Full) + .in(file(".")) + .settings( + name := "jewish-date", + version := "0.1.0", + libraryDependencies += "io.monix" %%% "minitest" % "2.0.0" % "test", + testFrameworks += new TestFramework("minitest.runner.Framework") + ) + .jvmSettings( + libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.13.5" % "test", + libraryDependencies += + ("KosherJava" % "zmanim" % "1.4.0alpha" % "test") + .from("https://github.com/KosherJava/zmanim/raw/master/lib/zmanim-1.4.0alpha.jar"), + Test / testOptions += Tests.Argument(TestFrameworks.ScalaCheck, "-minSuccessfulTests", "10000") + ) + .jsSettings( + libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.0.0-M12" + ) +lazy val jewishDateJS = jewishDate.js +lazy val jewishDateJVM = jewishDate.jvm diff --git a/jvm/src/test/scala/jewishdate/JewishDateCompanionProps.scala b/jvm/src/test/scala/jewishdate/JewishDateCompanionProps.scala new file mode 100644 index 0000000..13affd0 --- /dev/null +++ b/jvm/src/test/scala/jewishdate/JewishDateCompanionProps.scala @@ -0,0 +1,26 @@ +package jewishdate + +import java.time.{LocalDate, Month, Year} +import java.util.GregorianCalendar + +import net.sourceforge.zmanim.hebrewcalendar.{JewishCalendar => KJCal} +import org.scalacheck.Prop._ +import org.scalacheck.{Gen, Prop, Properties} + + +class JewishDateCompanionProps extends Properties("JewishDate") { + val genLocalDate: Gen[LocalDate] = + for { + year <- Gen.choose(1, 3000) + month <- Gen.choose(1, 12) + day <- Gen.choose(1, Month.of(month).length(Year.isLeap(year))) + } yield LocalDate.of(year, month, day) + + property("apply") = + Prop.forAll(genLocalDate) { date => + val jc = new KJCal(new GregorianCalendar(date.getYear, date.getMonthValue - 1, date.getDayOfMonth)) + val jd = JewishDate(date) + jd ?= + JewishDate(new JewishYear(jc.getJewishYear), JewishMonth(jc.getJewishMonth), jc.getJewishDayOfMonth) + } +} diff --git a/jvm/src/test/scala/jewishdate/JewishDateProps.scala b/jvm/src/test/scala/jewishdate/JewishDateProps.scala new file mode 100644 index 0000000..d28f2b5 --- /dev/null +++ b/jvm/src/test/scala/jewishdate/JewishDateProps.scala @@ -0,0 +1,25 @@ +package jewishdate + +import java.time._ + +import net.sourceforge.zmanim.hebrewcalendar.{JewishCalendar => KJCal} +import org.scalacheck.Prop._ +import org.scalacheck.{Gen, Prop, Properties} + + +class JewishDateProps extends Properties("jewishDate") { + val genJewishDate: Gen[JewishDate] = + for { + year <- Gen.choose(3762, 6000).map(new JewishYear(_)) + month <- Gen.oneOf(year.monthsIterator.toSeq) + day <- Gen.choose(1, year.monthLength(month)) + } yield JewishDate(year, month, day) + + + property("toLocalDate") = + Prop.forAll(genJewishDate) { date => + val jc = new KJCal(date.year.value, date.month.id, date.dayOfMonth) + date.toLocalDate ?= + LocalDate.of(jc.getGregorianYear, jc.getGregorianMonth + 1, jc.getGregorianDayOfMonth) + } +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..c4dc11b --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.1.0 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..8483fc5 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.21") diff --git a/shared/src/main/scala/jewishdate/JewishDate.scala b/shared/src/main/scala/jewishdate/JewishDate.scala new file mode 100644 index 0000000..e148b98 --- /dev/null +++ b/shared/src/main/scala/jewishdate/JewishDate.scala @@ -0,0 +1,63 @@ +package jewishdate + +import java.time.LocalDate + + +case class JewishDate(year: JewishYear, month: JewishMonth.Value, dayOfMonth: Int) { + def dayOfYear: Int = { + val priorMonths = year.monthsIterator(JewishMonth.Tishrei).takeWhile(_ != month) + priorMonths.map(year.monthLength).sum + dayOfMonth + } + + def isYomTov(yomTov: YomTov) = { + val d = day - yomTov.startDate.day + d >= 0 && d < yomTov.length + } + + def yomTov = year.yomimTovim.find(isYomTov) + + /** + * This day, counting from the first day of the Jewish calendar + */ + lazy val day = year.firstDay + dayOfYear + + def next = JewishDate.fromDay(day + 1) + def prev = JewishDate.fromDay(day - 1) + + def toLocalDate: LocalDate = LocalDate.ofEpochDay(day + JewishDate.JewishEpoch) +} + +object JewishDate { + /** + * The start of the Jewish calendar as Epoch Day + */ + val JewishEpoch = -2092592L + + def apply(year: Int, month: JewishMonth.Value, dayOfMonth: Int, dummy: Null = null): JewishDate = + new JewishDate(new JewishYear(year), month, dayOfMonth) + + def fromDay(jewishDay: Long): JewishDate = { + val jewishYearGuess = new JewishYear((jewishDay / 366).toInt) + val jewishYear = + Iterator.iterate(jewishYearGuess)(_.next) + .dropWhile(_.next.firstDay < jewishDay) + .next + + val monthSearchStart = + if (jewishDay < new JewishDate(jewishYear, JewishMonth.Nissan, 1).day) + JewishMonth.Tishrei + else + JewishMonth.Nissan + val jewishMonth = + jewishYear.monthsIterator(monthSearchStart) + .dropWhile(m => jewishDay > new JewishDate(jewishYear, m, jewishYear.monthLength(m)).day) + .next + val firstDayOfMonth = new JewishDate(jewishYear, jewishMonth, 1) + val actualDayOfMonth = jewishDay - firstDayOfMonth.day + 1 + firstDayOfMonth.copy(dayOfMonth = actualDayOfMonth.toInt) + } + + def apply(localDate: LocalDate): JewishDate = fromDay(localDate.toEpochDay - JewishEpoch) + + implicit val ordering: Ordering[JewishDate] = Ordering.by(_.day) +} diff --git a/shared/src/main/scala/jewishdate/JewishMonth.scala b/shared/src/main/scala/jewishdate/JewishMonth.scala new file mode 100644 index 0000000..a4391c1 --- /dev/null +++ b/shared/src/main/scala/jewishdate/JewishMonth.scala @@ -0,0 +1,6 @@ +package jewishdate + +object JewishMonth extends Enumeration(1) { + val Nissan, Iyar, Sivan, Tammuz, Av, Elul, Tishrei, Cheshvan, Kislev, Teves, Shvat, Adar = Value + val `Adar Sheni` = Value("Adar Sheni") +} diff --git a/shared/src/main/scala/jewishdate/JewishYear.scala b/shared/src/main/scala/jewishdate/JewishYear.scala new file mode 100644 index 0000000..4cba88a --- /dev/null +++ b/shared/src/main/scala/jewishdate/JewishYear.scala @@ -0,0 +1,132 @@ +package jewishdate + +import java.time.DayOfWeek + + +class JewishYear(val value: Int) extends AnyVal { + override def toString = value.toString + + def next = new JewishYear(value + 1) + + def prev = new JewishYear(value - 1) + + def isLeap = (7 * value + 1) % 19 < 7 + + def numMonths = if (isLeap) 13 else 12 + + /** + * The first day of this year, counting from the first day of the Jewish calendar + */ + def firstDay = { + val monthsPriorMetonicCycles = 235 * ((value - 1) / 19) + val nonLeapMonthsThisCycle = 12 * ((value - 1) % 19) + val leapMonthsThisCycle = (7 * ((value - 1) % 19) + 1) / 19 + val monthsPassed = monthsPriorMetonicCycles + nonLeapMonthsThisCycle + leapMonthsThisCycle + val chalakimFromMoladTohu = JewishYear.ChalakimMoladTohu + JewishYear.ChalakimPerMonth * monthsPassed.toLong + val moladDay = (chalakimFromMoladTohu / JewishYear.ChalakimPerDay).toInt + val moladChalakimInDay = (chalakimFromMoladTohu - moladDay.toLong * JewishYear.ChalakimPerDay).toInt + + // GaTRaD - If on a non leap year, the molad of Tishrei falls on a Tuesday (Ga) on or after 9 hours (T) and 204 + // chalakim (RaD) it is delayed till Thursday (one day delay, plus one day for Lo ADU Rosh) + val dechiyaGaTRaD = !isLeap && moladDay % 7 == 2 && moladChalakimInDay >= 9924 + + // BeTuTaKFoT - if the year following a leap year falls on a Monday (Be) on or after 15 hours (Tu) and 589 + // chalakim (TaKFoT) it is delayed till Tuesday + val dechiyaBeTuTaKFot = prev.isLeap && moladDay % 7 == 1 && moladChalakimInDay >= 16789 + + // Molad Zaken - If the molad of Tishrei falls after 12 noon, Rosh Hashana is delayed to the following day. If + // the following day is ADU, it will be delayed an additional day. + val dechiyaMoladZaken = moladChalakimInDay >= 19440 + + val day0 = if (dechiyaMoladZaken || dechiyaGaTRaD || dechiyaBeTuTaKFot) moladDay + 1 else moladDay + + // Lo ADU Rosh - Rosh Hashana can't fall on a Sunday, Wednesday or Friday. If the molad fell on one of these + // days, Rosh Hashana is delayed to the following day. + val dechiyaLoADURosh = day0 % 7 == 0 || day0 % 7 == 3 || day0 % 7 == 5 + + val withDechiyos = if (dechiyaLoADURosh) day0 + 1 else day0 + + withDechiyos + } + + /** + * The number of days in this Jewish year + */ + def length = next.firstDay - firstDay + + def isCheshvanLong = length % 10 == 5 + + def isKislevLong = length % 10 != 3 + + /** + * Returns an Iterator of the months of this year, starting from Nissan + */ + def monthsIterator: Iterator[JewishMonth.Value] = JewishMonth.values.iterator.take(numMonths) + + /** + * Retuns an Iterator of the months of this year, starting from the given month. + */ + def monthsIterator(first: JewishMonth.Value): Iterator[JewishMonth.Value] = { + val (before, fromStart) = monthsIterator.span(_ != first) + fromStart ++ before + } + + def monthLength(month: JewishMonth.Value) = month match { + case JewishMonth.Nissan | JewishMonth.Sivan | JewishMonth.Av | JewishMonth.Tishrei | JewishMonth.Shvat => 30 + case JewishMonth.Cheshvan if isCheshvanLong => 30 + case JewishMonth.Kislev if isKislevLong => 30 + case JewishMonth.Adar if isLeap => 30 + case _ => 29 + } + + private def date(month: JewishMonth.Value, dayOfMonth: Int) = JewishDate(this, month, dayOfMonth) + + def pesachFirst = YomTov("Pesach (first days)", date(JewishMonth.Nissan, 15), 2, melachaForbidden = true) + def pesachCholHamoed = YomTov("Chol Hamoed Pesach", date(JewishMonth.Nissan, 17), 4, melachaForbidden = false) + def pesachLast = YomTov("Pesach (last days)", date(JewishMonth.Nissan, 21), 2, melachaForbidden = true) + def shavuos = YomTov("Shavuos", date(JewishMonth.Sivan, 6), 2, melachaForbidden = true) + def tishaBAv = { + val d = date(JewishMonth.Av, 9) + val d2 = if (d.toLocalDate.getDayOfWeek == DayOfWeek.SATURDAY) d.next else d + YomTov("Tisha B'Av", d2, 1, melachaForbidden = false) + } + def roshHashanah = YomTov("Rosh Hashanah", date(JewishMonth.Tishrei, 1), 2, melachaForbidden = true) + def yomKippur = YomTov("Yom Kippur", date(JewishMonth.Tishrei, 10), 1, melachaForbidden = true) + def sukkosFirst = YomTov("Sukkos (first days)", date(JewishMonth.Tishrei, 15), 2, melachaForbidden = true) + def sukkosCholHamoed = + YomTov("Chol Hamoed Sukkos", date(JewishMonth.Tishrei, 17), 4, melachaForbidden = false) + def hoshanahRabbah = YomTov("Hoshanah Rabbah", date(JewishMonth.Tishrei, 21), 1, melachaForbidden = false) + def sheminiAtzeres = YomTov("Shemini Atzeres", date(JewishMonth.Tishrei, 22), 1, melachaForbidden = true) + def simchasTorah = YomTov("Simchas Torah", date(JewishMonth.Tishrei, 23), 1, melachaForbidden = true) + def chanukah = YomTov("Chanukah", date(JewishMonth.Kislev, 25), 8, melachaForbidden = false) + def purim = + YomTov("Purim", date(if (isLeap) JewishMonth.`Adar Sheni` else JewishMonth.Adar, 14), 1, melachaForbidden = false) + def shushanPurim = + YomTov("Shushan Purim", date(if (isLeap) JewishMonth.`Adar Sheni` else JewishMonth.Adar, 15), 1, melachaForbidden = false) + + def yomimTovim = { + Seq( + pesachFirst, + pesachCholHamoed, + pesachLast, + shavuos, + roshHashanah, + yomKippur, + sukkosFirst, + sukkosCholHamoed, + hoshanahRabbah, + sheminiAtzeres, + simchasTorah, + chanukah, + purim, + shushanPurim + ) + } +} + +object JewishYear { + val ChalakimMoladTohu = 31524L + val ChalakimPerMonth = 765433L + val ChalakimPerDay = 25920L +} + diff --git a/shared/src/main/scala/jewishdate/YomTov.scala b/shared/src/main/scala/jewishdate/YomTov.scala new file mode 100644 index 0000000..30a303d --- /dev/null +++ b/shared/src/main/scala/jewishdate/YomTov.scala @@ -0,0 +1,3 @@ +package jewishdate + +case class YomTov(name: String, startDate: JewishDate, length: Int, melachaForbidden: Boolean) diff --git a/shared/src/test/scala/jewishdate/ConversionExamples.scala b/shared/src/test/scala/jewishdate/ConversionExamples.scala new file mode 100644 index 0000000..bb297f8 --- /dev/null +++ b/shared/src/test/scala/jewishdate/ConversionExamples.scala @@ -0,0 +1,18 @@ +package jewishdate + +import java.time.LocalDate + +import minitest.SimpleTestSuite + + +object ConversionExamples extends SimpleTestSuite { + test("Example 1") { + assertEquals(JewishDate(LocalDate.of(842, 3, 31)), JewishDate(4602, JewishMonth.Nissan, 12)) + } + test("Example 2") { + assertEquals(JewishDate(LocalDate.of(1582, 10, 11)), JewishDate(5343, JewishMonth.Tishrei, 15)) + } + test("Example 3") { + assertEquals(JewishDate(LocalDate.of(1582, 10, 14)), JewishDate(5343, JewishMonth.Tishrei, 18)) + } +} diff --git a/shared/src/test/scala/jewishdate/YomTovExamples.scala b/shared/src/test/scala/jewishdate/YomTovExamples.scala new file mode 100644 index 0000000..27662ae --- /dev/null +++ b/shared/src/test/scala/jewishdate/YomTovExamples.scala @@ -0,0 +1,12 @@ +package jewishdate + +import minitest.SimpleTestSuite + + +object YomTovExamples extends SimpleTestSuite { + test("Yomim Tovim") { + val year = new JewishYear(5778) + assert(JewishDate(year, JewishMonth.Nissan, 22).isYomTov(year.pesachLast)) + assert(JewishDate(year, JewishMonth.Nissan, 23).yomTov.isEmpty) + } +}