Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/additional algo #6

Merged
merged 3 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ plugins {
group = "org.javafreedom"
project.version = project.findProperty("version") as String? ?: "0.0.1-SNAPSHOT"

val kotlinxDatetimeVersion = "0.6.1"
val konsistVersion = "0.17.3"

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:${kotlinxDatetimeVersion}")

testImplementation(kotlin("test"))

testImplementation("com.lemonappdev:konsist:${konsistVersion}")
}

tasks.test {
Expand All @@ -39,7 +44,7 @@ val version: String by lazy {
mavenPublishing {
println("Using Version: ${version}")

coordinates(group.toString(), rootProject.name, version)
coordinates(groupId = group.toString(), artifactId = rootProject.name, version = version)

publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

Expand Down
22 changes: 22 additions & 0 deletions src/main/kotlin/org/javafreedom/khol/Declaration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import kotlinx.datetime.LocalDate
import kotlinx.datetime.plus
import org.javafreedom.khol.algorithm.FirstAdvent
import org.javafreedom.khol.algorithm.GregorianEasterSundayGauss
import org.javafreedom.khol.algorithm.OrthodoxEaster

/**
* A declaration of a single Holiday, meaning the attributes when and where it is valid and how to calculate it.
*
* The conreteForYear method calculates the given holiday for any specific given year using the properties already
* present in the concrete class.
*/
sealed class Declaration(open val name: String,
open val validFromYear: Int,
open val validIn: Set<String>) {
Expand Down Expand Up @@ -38,6 +45,21 @@ sealed class Declaration(open val name: String,
}
}

data class OrthodoxEasterBasedHoliday(
val offset: Int,
override val name: String,
override val validFromYear: Int = 1990,
override val validIn: Set<String> = emptySet()
) : Declaration(name, validFromYear, validIn) {

private val orthodoxEaster = OrthodoxEaster()

override fun concreteForYear(year: Int): LocalDate {
val value = orthodoxEaster.calculateBaseDate(year).plus(this.offset, DateTimeUnit.DAY)
return LocalDate(year, value.month, value.dayOfMonth)
}
}

data class AdventBasedHoliday(
val offset: Int,
override val name: String,
Expand Down
26 changes: 24 additions & 2 deletions src/main/kotlin/org/javafreedom/khol/KHol.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import kotlinx.datetime.LocalDate
/**
* Some doc
*/
class KHol(val holidays: HolidayDeclarations, val year: Int, val validIn: String) {
class KHol(private val holidays: HolidayDeclarations, private val validIn: String) {

fun validHolidays(year: Int) : List<LocalDate> {
if (year < VALID_START_YEAR) throw KHolException("Year should be after 1989")

fun validHolidays() : List<LocalDate> {
val result = mutableListOf<LocalDate>()

holidays.declarations().filter {
Expand All @@ -19,4 +21,24 @@ class KHol(val holidays: HolidayDeclarations, val year: Int, val validIn: String

return result
}

/**
* All Holidays which are greater then, or equal to start and less then end are returned
*/
fun validHolidays(start: LocalDate, end: LocalDate) : List<LocalDate> {
if (start >= end) throw KHolException("Start Date should be before to End Date")

val result = mutableListOf<LocalDate>()
for (year in start.year..end.year) {
validHolidays(year)
.filter { it >= start && it < end }
.forEach { result.add(it) }
}

return result
}

companion object {
const val VALID_START_YEAR = 1990
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/org/javafreedom/khol/KHolException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.javafreedom.khol

class KHolException(msg: String): RuntimeException(msg)
21 changes: 11 additions & 10 deletions src/main/kotlin/org/javafreedom/khol/algorithm/FirstAdvent.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
@file:Suppress("detekt:style:MagicNumber")
package org.javafreedom.khol.algorithm

import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.plus
import kotlinx.datetime.minus
import org.javafreedom.khol.BaseCalculationAlgorithm

/**
* The First Sunday of Advent is the fourth Sunday before Christmas Day.
*/
class FirstAdvent : BaseCalculationAlgorithm {

override fun calculateBaseDate(year: Int): LocalDate {
// Find November 30th of the given year
val november30 = LocalDate(year, 11, 30)
// Get the day of the week for November 30th
val dayOfWeek = november30.dayOfWeek
// Calculate how many days to add to get to the nearest Sunday (Advent starts)
val daysToSunday = (DayOfWeek.SUNDAY.value - dayOfWeek.value + 7) % 7
// The first Advent Sunday is the Sunday closest to or on November 30th
return november30.plus(daysToSunday, DateTimeUnit.DAY)
// Find Christmas Day of the given year
val christmas = LocalDate(year, 12, 25)
// Find the Sunday before Christmas, which is the Fourth Sunday of Advent
val fourthAdvent = christmas.minus((christmas.dayOfWeek.value % 7).toLong(),
DateTimeUnit.DAY)
// Subtract 21 days (3 weeks) to get the First Sunday of Advent
return fourthAdvent.minus(3, DateTimeUnit.WEEK)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,47 @@ package org.javafreedom.khol.algorithm
import kotlinx.datetime.LocalDate
import org.javafreedom.khol.BaseCalculationAlgorithm

/**
* Calculates Easter Sunday for a given year using the algorithm by Carl Friedrich Gauss.
*
* This algorithm determines the date of Easter Sunday according to the rules
* of the Western Christian Church, which follows the Gregorian calendar.
*/
class GregorianEasterSundayGauss : BaseCalculationAlgorithm {

override fun calculateBaseDate(year: Int): LocalDate {
// Golden number (position of the year in the 19-year Metonic cycle)
val goldenNumber = year % 19

// Century (the century the year falls in)
val century = year / 100

// Year within the century
val yearInCentury = year % 100

// Number of leap years in the century
val leapYearsInCentury = century / 4

// Remainder when dividing century by 4 (used for correction)
val centuryRemainder = century % 4

// Adjustment factor for the century based on an offset
val centuryAdjustment = (century + 8) / 25

// Correction for the century’s alignment with the lunar-solar cycle
val centuryCycleCorrection = (century - centuryAdjustment + 1) / 3

// Days after March 21 that Easter Sunday could fall
val daysAfterEquinox = (19 * goldenNumber + century
- leapYearsInCentury - centuryCycleCorrection + 15) % 30

// Leap years in the current century (for finer adjustments)
val leapYearsInYear = yearInCentury / 4

// Remainder when dividing yearInCentury by 4
val yearInCenturyRemainder = yearInCentury % 4

// Additional adjustment based on leap years and the cycle corrections
val finalAdjustment = (32 + 2 * centuryRemainder + 2
* leapYearsInYear - daysAfterEquinox - yearInCenturyRemainder) % 7

// Final correction factor for the lunar cycle
val lunarCycleCorrection = (goldenNumber + 11 * daysAfterEquinox + 22
* finalAdjustment) / 451

// Determine the month of Easter (March or April)
val month = (daysAfterEquinox + finalAdjustment - 7 * lunarCycleCorrection + 114) / 31

// Determine the day of Easter Sunday
val day = ((daysAfterEquinox + finalAdjustment - 7 * lunarCycleCorrection + 114) % 31) + 1

// Return the calculated date of Easter Sunday
return LocalDate(year, month, day)
}

}
54 changes: 54 additions & 0 deletions src/main/kotlin/org/javafreedom/khol/algorithm/OrthodoxEaster.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@file:Suppress("detekt:style:MagicNumber")
package org.javafreedom.khol.algorithm

import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.plus
import org.javafreedom.khol.BaseCalculationAlgorithm

/**
* Orthodox Easter is determined based on the Julian calendar and follows the rule:
* It is the first Sunday after the Paschal Full Moon (first full moon after the vernal equinox).
* The result is converted to the Gregorian calendar for modern usage.
*
* * up until 1582, julian and gregorian easter dates were identical
* * after, 1 day is added for each century, except if the century year is exactly divisible by
* 400 (in which case no days are added). Safe until 4100 AD, when one leap day will be removed.
*
* Since we do not calculate Holidays before 1990, we just use the calculus for dates after 1700
*/
class OrthodoxEaster : BaseCalculationAlgorithm {

override fun calculateBaseDate(year: Int): LocalDate {
val julian = calculateJulian(year)

return julian.plus(julianToGregorianDifference(year), DateTimeUnit.DAY)
}

private fun calculateJulian(year: Int): LocalDate {
val g = year % 19
val i = (19 * g + 15) % 30
val j = (year + year / 4 + i) % 7
val month = 3 + (i - j + 40) / 44
val day = i - j + 28 - 31 * (month / 4)

return LocalDate(year, month, day)
}

private fun julianToGregorianDifference(year: Int): Int {
// The base year when the difference started (1582) - difference in days at that time was 10
val baseYear = 1582
// Difference in days (starts at 10 days for 1582, increases by 1 every 100 years except centuries
// divisible by 400)
var difference = 10

// Calculate how many centuries have passed since the base year
for (i in baseYear until year) {
if (i % 100 == 0 && i % 400 != 0) {
difference += 1
}
}
return difference
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@file:Suppress("detekt:style:MagicNumber")

package org.javafreedom.khol.declarations

import org.javafreedom.khol.Declaration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ public class KHolGermanHolidaysJavaTest {
@Test
void testNW2024() {
var holidayDeclarations = new GermanHolidayDeclarations();
var sut = new KHol(holidayDeclarations, 2024, "NW");
var sut = new KHol(holidayDeclarations, "NW");

var validHoldays = sut.validHolidays();
var validHoldays = sut.validHolidays(2024);

assertEquals(11, validHoldays.size());
assertTrue(validHoldays.stream().anyMatch(it ->
Expand Down
29 changes: 24 additions & 5 deletions src/test/kotlin/org/javafreedom/khol/DeclarationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import kotlinx.datetime.number
import kotlin.test.Test
import kotlin.test.assertEquals

internal data class DeclarationArguments(val year: Int, val month: Int, val day: Int, val days: Int)

private data class DeclarationArguments(val year: Int, val month: Int, val day: Int, val days: Int)

class DeclarationTest {
internal class DeclarationTest {

@Test
fun fixedHolidayToLocalDate() {
Expand All @@ -19,7 +18,7 @@ class DeclarationTest {
assertEquals(20, result.dayOfMonth)
}

private val argumentsList = mutableListOf(
private val gregorianEasterSundayArgs = mutableListOf(
DeclarationArguments(2008, 3, 23, 0),
DeclarationArguments(2015, 4, 6, 1),
DeclarationArguments(2017, 4, 15, -1),
Expand All @@ -29,7 +28,7 @@ class DeclarationTest {

@Test
fun easterBasedHolidayToLocalDate() {
argumentsList.forEach {
gregorianEasterSundayArgs.forEach {
val easterBased = Declaration.EasterBasedHoliday(it.days, "dummy")

val result = easterBased.concreteForYear(it.year)
Expand All @@ -38,4 +37,24 @@ class DeclarationTest {
assertEquals(it.day, result.dayOfMonth)
}
}

private val orthodoxEasterSundayArgs = mutableListOf(
DeclarationArguments(2008, 4, 27, 0),
DeclarationArguments(2015, 4, 13, 1),
DeclarationArguments(2017, 4, 15, -1),
DeclarationArguments(2018, 4, 14, 6),
DeclarationArguments(2019, 4, 26, -2),
DeclarationArguments(2027, 5, 2, 0))

@Test
fun orthodoxEasterBasedHolidayToLocalDate() {
orthodoxEasterSundayArgs.forEach {
val orthodoxEaster = Declaration.OrthodoxEasterBasedHoliday(it.days, "dummy")

val result = orthodoxEaster.concreteForYear(it.year)
assertEquals(it.year, result.year, "year comparison for ${it} vs. ${result}")
assertEquals(it.month, result.month.number, "month comparison for ${it} vs. ${result}")
assertEquals(it.day, result.dayOfMonth, "day comparison for ${it} vs. ${result}")
}
}
}
Loading
Loading