Skip to content

Commit 63c8395

Browse files
committed
Split ChronoUnit into sealed hierarchy of time-based and date-based units
1 parent 6da329a commit 63c8395

File tree

5 files changed

+119
-64
lines changed

5 files changed

+119
-64
lines changed

core/commonMain/src/ChronoUnit.kt

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,88 @@
55

66
package kotlinx.datetime
77

8+
import kotlin.time.Duration
9+
import kotlin.time.ExperimentalTime
10+
import kotlin.time.nanoseconds
811

9-
enum class TimeComponent {
10-
MONTH,
11-
DAY,
12-
NANOSECOND
13-
}
12+
// TODO: toString, equality
13+
// alternative name: Interval
14+
sealed class ChronoUnit {
15+
16+
abstract operator fun times(scalar: Int): ChronoUnit
17+
18+
internal abstract val calendarUnit: CalendarUnit
19+
internal abstract val calendarScale: Long
20+
21+
class TimeBased(val nanoseconds: Long) : ChronoUnit() {
22+
internal override val calendarUnit: CalendarUnit
23+
internal override val calendarScale: Long
1424

15-
class ChronoUnit(val scale: Long, val component: TimeComponent) {
16-
init {
17-
require(scale > 0) { "Unit scale must be positive, but was $scale" }
25+
init {
26+
require(nanoseconds > 0) { "Unit duration must be positive, but was $nanoseconds ns." }
27+
when {
28+
nanoseconds % 3600_000_000_000 == 0L -> {
29+
calendarUnit = CalendarUnit.HOUR
30+
calendarScale = nanoseconds / 3600_000_000_000
31+
}
32+
nanoseconds % 60_000_000_000 == 0L -> {
33+
calendarUnit = CalendarUnit.MINUTE
34+
calendarScale = nanoseconds / 60_000_000_000
35+
}
36+
nanoseconds % 1_000_000_000 == 0L -> {
37+
calendarUnit = CalendarUnit.SECOND
38+
calendarScale = nanoseconds / 1_000_000_000
39+
}
40+
else -> {
41+
calendarUnit = CalendarUnit.NANOSECOND
42+
calendarScale = nanoseconds
43+
}
44+
}
45+
}
46+
47+
override fun times(scalar: Int): TimeBased = TimeBased(nanoseconds * scalar) // TODO: prevent overflow
48+
49+
@ExperimentalTime
50+
val duration: Duration = nanoseconds.nanoseconds
1851
}
19-
constructor(number: Long, unit: ChronoUnit) : this(number * unit.scale, unit.component)
20-
// it seems possible to provide 'times' operation
21-
companion object {
22-
val NANOSECOND = ChronoUnit(1, TimeComponent.NANOSECOND)
23-
val MICROSECOND = ChronoUnit(1000, NANOSECOND)
24-
val MILLISECOND = ChronoUnit(1000, MICROSECOND)
25-
val SECOND = ChronoUnit(1000, MILLISECOND)
26-
val MINUTE = ChronoUnit(60, SECOND)
27-
val HOUR = ChronoUnit(60, MINUTE)
28-
val DAY = ChronoUnit(1, TimeComponent.DAY)
29-
val WEEK = ChronoUnit(7, DAY)
30-
val MONTH = ChronoUnit(1, TimeComponent.MONTH)
31-
val QUARTER = ChronoUnit(3, MONTH)
32-
val YEAR = ChronoUnit(12, MONTH)
33-
val CENTURY = ChronoUnit(100, YEAR)
52+
53+
sealed class DateBased : ChronoUnit() {
54+
// TODO: investigate how to move subclasses to ChronoUnit scope
55+
class DaysBased(val days: Int) : DateBased() {
56+
init {
57+
require(days > 0) { "Unit duration must be positive, but was $days days." }
58+
}
59+
60+
override fun times(scalar: Int): DaysBased = DaysBased(days * scalar)
61+
62+
internal override val calendarUnit: CalendarUnit get() = CalendarUnit.DAY
63+
internal override val calendarScale: Long get() = days.toLong()
64+
}
65+
class MonthsBased(val months: Int) : DateBased() {
66+
init {
67+
require(months > 0) { "Unit duration must be positive, but was $months months." }
68+
}
69+
70+
override fun times(scalar: Int): MonthsBased = MonthsBased(months * scalar)
71+
72+
internal override val calendarUnit: CalendarUnit get() = CalendarUnit.MONTH
73+
internal override val calendarScale: Long get() = months.toLong()
74+
}
3475
}
35-
}
3676

37-
internal fun TimeComponent.toCalendarUnit(): CalendarUnit = when(this) {
38-
TimeComponent.MONTH -> CalendarUnit.MONTH
39-
TimeComponent.DAY -> CalendarUnit.DAY
40-
TimeComponent.NANOSECOND -> CalendarUnit.NANOSECOND
77+
78+
companion object {
79+
val NANOSECOND = TimeBased(nanoseconds = 1)
80+
val MICROSECOND = NANOSECOND * 1000
81+
val MILLISECOND = MICROSECOND * 1000
82+
val SECOND = MILLISECOND * 1000
83+
val MINUTE = SECOND * 60
84+
val HOUR = MINUTE * 60
85+
val DAY = DateBased.DaysBased(days = 1)
86+
val WEEK = DAY * 7
87+
val MONTH = DateBased.MonthsBased(months = 1)
88+
val QUARTER = MONTH * 3
89+
val YEAR = MONTH * 12
90+
val CENTURY = YEAR * 100
91+
}
4192
}

core/commonMain/src/Instant.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ public fun Instant.minus(other: Instant, unit: CalendarUnit, zone: TimeZone): Lo
5050

5151

5252
public fun Instant.plus(unit: ChronoUnit, zone: TimeZone): Instant =
53-
plus(unit.scale, unit.component.toCalendarUnit(), zone)
53+
plus(unit.calendarScale, unit.calendarUnit, zone)
5454
public fun Instant.plus(value: Int, unit: ChronoUnit, zone: TimeZone): Instant =
55-
plus(value * unit.scale, unit.component.toCalendarUnit(), zone)
55+
plus(value * unit.calendarScale, unit.calendarUnit, zone)
5656
public fun Instant.plus(value: Long, unit: ChronoUnit, zone: TimeZone): Instant =
57-
plus(value * unit.scale, unit.component.toCalendarUnit(), zone)
57+
plus(value * unit.calendarScale, unit.calendarUnit, zone)
5858

5959
public fun Instant.until(other: Instant, unit: ChronoUnit, zone: TimeZone): Long =
60-
until(other, unit.component.toCalendarUnit(), zone) / unit.scale
60+
until(other, unit.calendarUnit, zone) / unit.calendarScale
61+
62+
public fun Instant.minus(other: Instant, unit: ChronoUnit, zone: TimeZone): Long = other.until(this, unit, zone)

core/commonMain/src/LocalDate.kt

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,14 @@ public fun LocalDate.until(other: LocalDate, unit: CalendarUnit): Int = when(uni
4343
throw UnsupportedOperationException("Only date based units can be used to express difference between LocalDate values.")
4444
}
4545

46-
public fun LocalDate.plus(unit: ChronoUnit): LocalDate =
47-
plus(unit.scale, unit.component.toCalendarUnit())
48-
public fun LocalDate.plus(value: Int, unit: ChronoUnit): LocalDate =
49-
plus(value * unit.scale, unit.component.toCalendarUnit())
50-
public fun LocalDate.plus(value: Long, unit: ChronoUnit): LocalDate =
51-
plus(value * unit.scale, unit.component.toCalendarUnit())
52-
53-
public fun LocalDate.until(other: LocalDate, unit: ChronoUnit): Int = when(unit.component) {
54-
TimeComponent.MONTH -> (monthsUntil(other) / unit.scale).toInt()
55-
TimeComponent.DAY -> (daysUntil(other) / unit.scale).toInt()
56-
TimeComponent.NANOSECOND ->
57-
throw UnsupportedOperationException("Only date based units can be used to express difference between LocalDate values.")
46+
public fun LocalDate.plus(unit: ChronoUnit.DateBased): LocalDate =
47+
plus(unit.calendarScale, unit.calendarUnit)
48+
public fun LocalDate.plus(value: Int, unit: ChronoUnit.DateBased): LocalDate =
49+
plus(value * unit.calendarScale, unit.calendarUnit)
50+
public fun LocalDate.plus(value: Long, unit: ChronoUnit.DateBased): LocalDate =
51+
plus(value * unit.calendarScale, unit.calendarUnit)
52+
53+
public fun LocalDate.until(other: LocalDate, unit: ChronoUnit.DateBased): Int = when(unit) {
54+
is ChronoUnit.DateBased.MonthsBased -> (monthsUntil(other) / unit.months).toInt()
55+
is ChronoUnit.DateBased.DaysBased -> (daysUntil(other) / unit.days).toInt()
5856
}

core/commonTest/src/InstantTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ class InstantTest {
109109
assertEquals(366 + 31 + 30, instant1.until(instant4, ChronoUnit.DAY, zone))
110110
assertEquals((366 + 31 + 30) * 24 + 1, instant1.until(instant4, ChronoUnit.HOUR, zone))
111111

112+
for (timeUnit in listOf(ChronoUnit.SECOND, ChronoUnit.MINUTE, ChronoUnit.HOUR)) {
113+
assertEquals(instant4 - instant1, timeUnit.duration * instant4.minus(instant1, timeUnit, zone).toDouble())
114+
}
115+
112116
val period = CalendarPeriod(days = 1, hours = 1)
113117
val instant5 = instant1.plus(period, zone)
114118
checkComponents(instant5.toLocalDateTime(zone), 2019, 10, 28, 3, 59)

core/commonTest/src/LocalDateTest.kt

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class LocalDateTest {
6262

6363
assertFailsWith<UnsupportedOperationException> { startDate + CalendarPeriod(hours = 7) }
6464
assertFailsWith<UnsupportedOperationException> { startDate.plus(7, CalendarUnit.HOUR) }
65-
assertFailsWith<UnsupportedOperationException> { startDate.plus(7, ChronoUnit.MINUTE) }
65+
// assertFailsWith<UnsupportedOperationException> { startDate.plus(7, ChronoUnit.MINUTE) } // won't compile
6666
}
6767

6868
@Test
@@ -93,21 +93,21 @@ class LocalDateTest {
9393

9494
fun until() {
9595
val data = listOf(
96-
Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.DAY, 0)),
97-
Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.WEEK, 0)),
98-
Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.MONTH, 0)),
99-
Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.YEAR, 0)),
100-
Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.DAY, 1)),
101-
Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.WEEK, 0)),
102-
Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.MONTH, 0)),
103-
Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.YEAR, 0)),
104-
Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.DAY, 7)),
105-
Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.WEEK, 1)),
106-
Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.MONTH, 0)),
107-
Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.YEAR, 0)),
108-
Pair(Pair("2012-06-30", "2012-07-29"), Pair(CalendarUnit.MONTH, 0)),
109-
Pair(Pair("2012-06-30", "2012-07-30"), Pair(CalendarUnit.MONTH, 1)),
110-
Pair(Pair("2012-06-30", "2012-07-31"), Pair(CalendarUnit.MONTH, 1)))
96+
Pair(Pair("2012-06-30", "2012-06-30"), Pair(ChronoUnit.DAY, 0)),
97+
Pair(Pair("2012-06-30", "2012-06-30"), Pair(ChronoUnit.WEEK, 0)),
98+
Pair(Pair("2012-06-30", "2012-06-30"), Pair(ChronoUnit.MONTH, 0)),
99+
Pair(Pair("2012-06-30", "2012-06-30"), Pair(ChronoUnit.YEAR, 0)),
100+
Pair(Pair("2012-06-30", "2012-07-01"), Pair(ChronoUnit.DAY, 1)),
101+
Pair(Pair("2012-06-30", "2012-07-01"), Pair(ChronoUnit.WEEK, 0)),
102+
Pair(Pair("2012-06-30", "2012-07-01"), Pair(ChronoUnit.MONTH, 0)),
103+
Pair(Pair("2012-06-30", "2012-07-01"), Pair(ChronoUnit.YEAR, 0)),
104+
Pair(Pair("2012-06-30", "2012-07-07"), Pair(ChronoUnit.DAY, 7)),
105+
Pair(Pair("2012-06-30", "2012-07-07"), Pair(ChronoUnit.WEEK, 1)),
106+
Pair(Pair("2012-06-30", "2012-07-07"), Pair(ChronoUnit.MONTH, 0)),
107+
Pair(Pair("2012-06-30", "2012-07-07"), Pair(ChronoUnit.YEAR, 0)),
108+
Pair(Pair("2012-06-30", "2012-07-29"), Pair(ChronoUnit.MONTH, 0)),
109+
Pair(Pair("2012-06-30", "2012-07-30"), Pair(ChronoUnit.MONTH, 1)),
110+
Pair(Pair("2012-06-30", "2012-07-31"), Pair(ChronoUnit.MONTH, 1)))
111111
for ((values, interval) in data) {
112112
val (v1, v2) = values
113113
val (unit, length) = interval
@@ -117,10 +117,10 @@ class LocalDateTest {
117117
assertEquals(-length, end.until(start, unit), "$v1 - $v2 = -$length($unit)")
118118
@Suppress("NON_EXHAUSTIVE_WHEN")
119119
when (unit) {
120-
CalendarUnit.YEAR -> assertEquals(length, start.yearsUntil(end))
121-
CalendarUnit.MONTH -> assertEquals(length, start.monthsUntil(end))
122-
CalendarUnit.WEEK -> assertEquals(length, start.daysUntil(end) / 7)
123-
CalendarUnit.DAY -> assertEquals(length, start.daysUntil(end))
120+
ChronoUnit.YEAR -> assertEquals(length, start.yearsUntil(end))
121+
ChronoUnit.MONTH -> assertEquals(length, start.monthsUntil(end))
122+
ChronoUnit.WEEK -> assertEquals(length, start.daysUntil(end) / 7)
123+
ChronoUnit.DAY -> assertEquals(length, start.daysUntil(end))
124124
}
125125
}
126126

0 commit comments

Comments
 (0)