Skip to content

Commit

Permalink
Enable construction of dates from week date representation (#106)
Browse files Browse the repository at this point in the history
* Enable construction of dates from week date representation

* Fix validation on week date construction

* Fix compile error
  • Loading branch information
erikc5000 committed Jul 25, 2020
1 parent 630a926 commit 789d42a
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
package io.islandtime

import io.islandtime.calendar.WeekSettings
import io.islandtime.internal.lengthOfWeekBasedYearImpl
import io.islandtime.internal.lengthOfWeekBasedYear
import io.islandtime.internal.weekBasedYearImpl
import io.islandtime.internal.weekOfMonthImpl
import io.islandtime.internal.weekOfWeekBasedYearImpl
Expand Down Expand Up @@ -86,7 +86,7 @@ fun Date.weekOfWeekBasedYear(settings: WeekSettings): Int = weekOfWeekBasedYearI
* The length of the ISO week-based year that this date falls in, either 52 or 53 weeks.
*/
val Date.lengthOfWeekBasedYear: IntWeeks
get() = lengthOfWeekBasedYearImpl
get() = lengthOfWeekBasedYear(weekBasedYear)

/**
* The week of the month (0-5) according to the ISO definition.
Expand Down
14 changes: 0 additions & 14 deletions core/src/commonMain/kotlin/io/islandtime/DateProperties.kt

This file was deleted.

17 changes: 8 additions & 9 deletions core/src/commonMain/kotlin/io/islandtime/DayOfWeek.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,19 @@ enum class DayOfWeek {
* The ISO week starts on Monday (1) and ends on Sunday (7).
*/
fun Int.toDayOfWeek(): DayOfWeek {
if (this !in DayOfWeek.MIN.number..DayOfWeek.MAX.number) {
throw DateTimeException("'$this' is not a valid day of week number")
}

return DayOfWeek.values()[this - 1]
return DayOfWeek.values()[checkValidDayOfWeek(this) - 1]
}

/**
* Convert a day of week number (1-7) to a [DayOfWeek] according to the week definition provided by [settings].
*/
fun Int.toDayOfWeek(settings: WeekSettings): DayOfWeek {
if (this !in DayOfWeek.MIN.number..DayOfWeek.MAX.number) {
throw DateTimeException("'$this' is not a valid day of week number")
}
return settings.firstDayOfWeek + (checkValidDayOfWeek(this) - 1).days
}

return settings.firstDayOfWeek + (this - 1).days
internal fun checkValidDayOfWeek(number: Int): Int {
if (number !in 1..7) {
throw DateTimeException("'$number' is not a valid day of week number")
}
return number
}
95 changes: 95 additions & 0 deletions core/src/commonMain/kotlin/io/islandtime/WeekDate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
@file:JvmMultifileClass
@file:JvmName("DateTimesKt")

package io.islandtime

import io.islandtime.calendar.WeekSettings
import io.islandtime.internal.lastWeekOfWeekBasedYear
import io.islandtime.measures.days
import io.islandtime.measures.weeks
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

/**
* Converts this date to an ISO week date representation.
*/
inline fun <T> Date.toWeekDate(action: (year: Int, week: Int, day: Int) -> T): T {
return action(weekBasedYear, weekOfWeekBasedYear, dayOfWeek.number)
}

/**
* Converts this date to a week date representation using the week definition in [settings].
*/
inline fun <T> Date.toWeekDate(settings: WeekSettings, action: (year: Int, week: Int, day: Int) -> T): T {
return action(weekBasedYear(settings), weekOfWeekBasedYear(settings), dayOfWeek.number(settings))
}

/**
* Create a [Date] from an ISO week date.
* @param year the week-based year
* @param week the week number of the week-based year
* @param day the ISO day of week number, 1 (Monday) to 7 (Sunday)
* @throws DateTimeException if the year, week, or day is invalid
*/
fun Date.Companion.fromWeekDate(year: Int, week: Int, day: Int): Date {
checkValidDayOfWeek(day)
checkValidYear(year)
checkValidWeekOfWeekBasedYear(week, year)
// TODO: The day number may exceed the max near the end of the year, but is it even worth checking?

val jan4 = Date(year, Month.JANUARY, 4)
val dayOfYear = (week * 7 + day) - (jan4.dayOfWeek.number + 3)

return if (dayOfYear < 1) {
Date(year = year - 1, dayOfYear = dayOfYear + lastDayOfYear(year - 1))
} else {
val lastDay = lastDayOfYear(year)

if (dayOfYear > lastDay) {
Date(year = year + 1, dayOfYear = dayOfYear - lastDay)
} else {
Date(year, dayOfYear)
}
}
}

/**
* Create a [Date] from a week date representation using the week definition in [settings].
* @param year the week-based year
* @param week the week number of the week-based year
* @param day the day of week number, 1-7
* @param settings the week definition to use when interpreting the [year], [week], and [day]
*/
fun Date.Companion.fromWeekDate(year: Int, week: Int, day: Int, settings: WeekSettings): Date {
checkValidDayOfWeek(day)

// Week dates around Date.MIN and Date.MAX can fail here if the week year exceeds the supported range, but no easy
// way to work around that.
checkValidYear(year)

// TODO: This allows the week number to be invalid for the year, but is it worth checking? The day number may also
// exceed the max near the end of the year.
checkValidWeekOfWeekBasedYear(week)

val date = Date(year, Month.JANUARY, day = settings.minimumDaysInFirstWeek)
val weeksToAdd = (week - date.weekOfYear(settings)).weeks
val daysToAdd = weeksToAdd + (day - date.dayOfWeek.number(settings)).days
return date + daysToAdd
}

private fun checkValidWeekOfWeekBasedYear(week: Int): Int {
if (week !in 1..53) {
throw DateTimeException("The week '$week' is outside the supported range of 1-53")
}
return week
}

private fun checkValidWeekOfWeekBasedYear(week: Int, year: Int): Int {
checkValidWeekOfWeekBasedYear(week)

if (week > lastWeekOfWeekBasedYear(year)) {
throw DateTimeException("Week 53 doesn't exist in $year")
}

return week
}
18 changes: 11 additions & 7 deletions core/src/commonMain/kotlin/io/islandtime/internal/WeekNumbers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,17 @@ internal inline fun Date.weekOfWeekBasedYearImpl(settings: WeekSettings): Int {
}
}

internal inline val Date.lengthOfWeekBasedYearImpl: IntWeeks
get() {
val startOfWeekBasedYear = Year(weekBasedYear).startDate
val dayOfWeek = startOfWeekBasedYear.dayOfWeek
val isLongYear = dayOfWeek == DayOfWeek.THURSDAY || (dayOfWeek == DayOfWeek.WEDNESDAY && isInLeapYear)
return if (isLongYear) 53.weeks else 52.weeks
}
internal fun lengthOfWeekBasedYear(weekBasedYear: Int): IntWeeks {
return lastWeekOfWeekBasedYear(weekBasedYear).weeks
}

internal fun lastWeekOfWeekBasedYear(weekBasedYear: Int): Int {
val year = Year(weekBasedYear)
val startOfWeekBasedYear = year.startDate
val dayOfWeek = startOfWeekBasedYear.dayOfWeek
val isLongYear = dayOfWeek == DayOfWeek.THURSDAY || (dayOfWeek == DayOfWeek.WEDNESDAY && year.isLeap)
return if (isLongYear) 53 else 52
}

private fun Date.weekNumber(dayOfMonthOrYear: Int, settings: WeekSettings): Int {
return weekNumber(dayOfWeek, dayOfMonthOrYear, settings)
Expand Down
73 changes: 21 additions & 52 deletions core/src/commonTest/kotlin/io/islandtime/DatePropertiesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.islandtime
import io.islandtime.calendar.WeekSettings
import io.islandtime.measures.weeks
import io.islandtime.test.AbstractIslandTimeTest
import io.islandtime.test.TestData
import kotlin.test.Test
import kotlin.test.assertEquals

Expand All @@ -13,7 +14,8 @@ class DatePropertiesTest : AbstractIslandTimeTest() {
Date(2008, 12, 31) to 5,
Date(2009, 1, 1) to 1,
Date(2009, 1, 4) to 1,
Date(2009, 1, 5) to 2
Date(2009, 1, 5) to 2,
Date(2020, 5, 31) to 4
).forEach { (date, week) ->
assertEquals(week, date.weekOfMonth, date.toString())
}
Expand All @@ -27,7 +29,8 @@ class DatePropertiesTest : AbstractIslandTimeTest() {
Date(2008, 12, 31) to 5,
Date(2009, 1, 1) to 1,
Date(2009, 1, 3) to 1,
Date(2009, 1, 4) to 2
Date(2009, 1, 4) to 2,
Date(2020, 5, 31) to 6
).forEach { (date, week) ->
assertEquals(week, date.weekOfMonth(WeekSettings.SUNDAY_START), date.toString())
}
Expand All @@ -41,7 +44,8 @@ class DatePropertiesTest : AbstractIslandTimeTest() {
Date(2008, 12, 31) to 5,
Date(2009, 1, 1) to 0,
Date(2009, 1, 4) to 0,
Date(2009, 1, 5) to 1
Date(2009, 1, 5) to 1,
Date(2020, 5, 31) to 4
).forEach { (date, week) ->
assertEquals(
week,
Expand Down Expand Up @@ -97,62 +101,27 @@ class DatePropertiesTest : AbstractIslandTimeTest() {

@Test
fun `ISO week date`() {
listOf(
Date(2005, 1, 1) to Triple(2004, 53, 6),
Date(2005, 1, 2) to Triple(2004, 53, 7),
Date(2005, 12, 31) to Triple(2005, 52, 6),
Date(2006, 1, 1) to Triple(2005, 52, 7),
Date(2006, 1, 2) to Triple(2006, 1, 1),
Date(2006, 12, 31) to Triple(2006, 52, 7),
Date(2007, 1, 1) to Triple(2007, 1, 1),
Date(2007, 12, 30) to Triple(2007, 52, 7),
Date(2007, 12, 31) to Triple(2008, 1, 1),
Date(2008, 1, 1) to Triple(2008, 1, 2),
Date(2008, 12, 28) to Triple(2008, 52, 7),
Date(2008, 12, 29) to Triple(2009, 1, 1),
Date(2008, 12, 30) to Triple(2009, 1, 2),
Date(2008, 12, 31) to Triple(2009, 1, 3),
Date(2009, 1, 1) to Triple(2009, 1, 4),
Date(2009, 12, 31) to Triple(2009, 53, 4),
Date(2010, 1, 1) to Triple(2009, 53, 5),
Date(2010, 1, 2) to Triple(2009, 53, 6),
Date(2010, 1, 3) to Triple(2009, 53, 7),
Date(2010, 1, 4) to Triple(2010, 1, 1)
).forEach { (date, weekDate) ->
assertEquals(weekDate, date.toWeekDate(::Triple), "toWeekDate(): $date")

TestData.isoWeekDates.forEach { (date, weekDate) ->
val (year, week) = weekDate
assertEquals(year, date.weekBasedYear, "weekBasedYear: $date")
assertEquals(week, date.weekOfWeekBasedYear, "weekOfWeekBasedYear: $date")

assertEquals(
Pair(year, week),
Pair(date.weekBasedYear, date.weekOfWeekBasedYear),
date.toString()
)
}
}

@Test
fun `week date with Sunday start`() {
listOf(
Date(2016, 12, 30) to Pair(2016, 53),
Date(2016, 12, 31) to Pair(2016, 53),
Date(2017, 1, 1) to Pair(2017, 1),
Date(2017, 1, 2) to Pair(2017, 1),
Date(2017, 1, 7) to Pair(2017, 1),
Date(2017, 1, 8) to Pair(2017, 2),
Date(2017, 12, 30) to Pair(2017, 52),
Date(2017, 12, 31) to Pair(2018, 1),
Date(2018, 1, 6) to Pair(2018, 1),
Date(2018, 12, 29) to Pair(2018, 52),
Date(2018, 12, 30) to Pair(2019, 1),
Date(2019, 1, 5) to Pair(2019, 1),
Date(2019, 1, 6) to Pair(2019, 2),
Date(2019, 12, 28) to Pair(2019, 52),
Date(2019, 12, 29) to Pair(2020, 1),
Date(2020, 1, 5) to Pair(2020, 2)
).forEach { (date, yearWeek) ->
val settings = WeekSettings.SUNDAY_START

TestData.sundayStartWeekDates.forEach { (date, weekDate) ->
val (year, week) = weekDate

assertEquals(
yearWeek,
Pair(
date.weekBasedYear(WeekSettings.SUNDAY_START),
date.weekOfWeekBasedYear(WeekSettings.SUNDAY_START)
),
Pair(year, week),
Pair(date.weekBasedYear(settings), date.weekOfWeekBasedYear(settings)),
date.toString()
)
}
Expand Down
81 changes: 81 additions & 0 deletions core/src/commonTest/kotlin/io/islandtime/WeekDateTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.islandtime

import io.islandtime.calendar.WeekSettings.Companion.SUNDAY_START
import io.islandtime.test.AbstractIslandTimeTest
import io.islandtime.test.TestData
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class WeekDateTest : AbstractIslandTimeTest() {
@Test
fun `Date_toWeekDate() converts to ISO week date`() {
TestData.isoWeekDates.forEach { (date, weekDate) ->
assertEquals(weekDate, date.toWeekDate(::Triple), date.toString())
}
}

@Test
fun `Date_toWeekDate() converts to week date with Sunday start week definition`() {
TestData.sundayStartWeekDates.forEach { (date, weekDate) ->
assertEquals(weekDate, date.toWeekDate(SUNDAY_START, ::Triple), date.toString())
}
}

@Test
fun `Date_fromWeekDate() throws an exception when year is out of range`() {
assertFailsWith<DateTimeException> { Date.fromWeekDate(Year.MIN_VALUE - 1, 52, 1) }
assertFailsWith<DateTimeException> { Date.fromWeekDate(Year.MAX_VALUE + 1, 1, 1) }
}

@Test
fun `Date_fromWeekDate(settings) throws an exception when year is out of range`() {
assertFailsWith<DateTimeException> {
Date.fromWeekDate(Year.MIN_VALUE - 1, 52, 1, SUNDAY_START)
}
assertFailsWith<DateTimeException> {
Date.fromWeekDate(Year.MAX_VALUE + 1, 1, 1, SUNDAY_START)
}
}

@Test
fun `Date_fromWeekDate() throws an exception when week is out of range`() {
assertFailsWith<DateTimeException> { Date.fromWeekDate(2000, 0, 1) }
assertFailsWith<DateTimeException> { Date.fromWeekDate(2010, 53, 1) }
assertFailsWith<DateTimeException> { Date.fromWeekDate(2010, 54, 1) }
}

@Test
fun `Date_fromWeekDate(settings) throws an exception when week is out of range`() {
assertFailsWith<DateTimeException> { Date.fromWeekDate(2000, 0, 1, SUNDAY_START) }
assertFailsWith<DateTimeException> { Date.fromWeekDate(2010, 54, 1, SUNDAY_START) }
}

@Test
fun `Date_fromWeekDate() throws an exception when day is out of range`() {
assertFailsWith<DateTimeException> { Date.fromWeekDate(2000, 23, 0) }
assertFailsWith<DateTimeException> { Date.fromWeekDate(2010, 35, 8) }
}

@Test
fun `Date_fromWeekDate(settings) throws an exception when day is out of range`() {
assertFailsWith<DateTimeException> { Date.fromWeekDate(2000, 23, 0, SUNDAY_START) }
assertFailsWith<DateTimeException> { Date.fromWeekDate(2010, 35, 8, SUNDAY_START) }
}

@Test
fun `Date_fromWeekDate() creates a Date from an ISO week date`() {
TestData.isoWeekDates.forEach { (date, weekDate) ->
val (year, week, day) = weekDate
assertEquals(date, Date.fromWeekDate(year, week, day), date.toString())
}
}

@Test
fun `Date_fromWeekDate() creates a Date from a Sunday start week date`() {
TestData.sundayStartWeekDates.filter { it.first != Date.MAX }.forEach { (date, weekDate) ->
val (year, week, day) = weekDate
assertEquals(date, Date.fromWeekDate(year, week, day, SUNDAY_START), date.toString())
}
}
}
Loading

0 comments on commit 789d42a

Please sign in to comment.