Skip to content

Commit

Permalink
Initial commit -- 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
nafg committed Jan 8, 2018
0 parents commit f530bcf
Show file tree
Hide file tree
Showing 15 changed files with 340 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea/
target/
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
jdk:
- oraclejdk8
language: scala
script: "sbt publish"
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions bintray.sbt
Original file line number Diff line number Diff line change
@@ -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
)
}
25 changes: 25 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions jvm/src/test/scala/jewishdate/JewishDateCompanionProps.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
25 changes: 25 additions & 0 deletions jvm/src/test/scala/jewishdate/JewishDateProps.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version = 1.1.0
1 change: 1 addition & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.21")
63 changes: 63 additions & 0 deletions shared/src/main/scala/jewishdate/JewishDate.scala
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 6 additions & 0 deletions shared/src/main/scala/jewishdate/JewishMonth.scala
Original file line number Diff line number Diff line change
@@ -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")
}
132 changes: 132 additions & 0 deletions shared/src/main/scala/jewishdate/JewishYear.scala
Original file line number Diff line number Diff line change
@@ -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
}

3 changes: 3 additions & 0 deletions shared/src/main/scala/jewishdate/YomTov.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package jewishdate

case class YomTov(name: String, startDate: JewishDate, length: Int, melachaForbidden: Boolean)
18 changes: 18 additions & 0 deletions shared/src/test/scala/jewishdate/ConversionExamples.scala
Original file line number Diff line number Diff line change
@@ -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))
}
}
12 changes: 12 additions & 0 deletions shared/src/test/scala/jewishdate/YomTovExamples.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit f530bcf

Please sign in to comment.