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

Enable construction of dates from week date representation #106

Merged
merged 3 commits into from
Jul 25, 2020
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
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