@@ -6,40 +6,44 @@ package kotlinx.datetime.test
66
77import kotlinx.datetime.*
88import kotlinx.cinterop.*
9+ import kotlinx.datetime.internal.NANOS_PER_ONE
910import platform.Foundation.*
1011import kotlin.math.*
1112import kotlin.random.*
1213import kotlin.test.*
1314
1415class ConvertersTest {
1516
16- private val dateFormatter = NSDateFormatter ()
17- private val locale = NSLocale .localeWithLocaleIdentifier(" en_US_POSIX" )
18- private val gregorian = NSCalendar .calendarWithIdentifier(NSCalendarIdentifierGregorian )!!
17+ private val isoCalendar = NSCalendar .calendarWithIdentifier(NSCalendarIdentifierISO8601 )!!
18+ @OptIn(UnsafeNumber ::class )
1919 private val utc = NSTimeZone .timeZoneForSecondsFromGMT(0 )
2020
21- init {
22- dateFormatter.locale = locale
23- dateFormatter.dateFormat = " yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
24- dateFormatter.calendar = gregorian
25- dateFormatter.timeZone = utc
21+ @Test
22+ fun testToFromNSDateNow () {
23+ // as of writing, the max difference in such round-trips across 10^8 iterations was 120 nanoseconds,
24+ // so we allow for four times that
25+ repeat(10 ) {
26+ val now = Clock .System .now()
27+ assertEqualUpToHalfMicrosecond(now, now.toNSDate().toKotlinInstant())
28+ }
29+ repeat(10 ) {
30+ val nowInSwift = NSDate ()
31+ assertEqualUpToHalfMicrosecond(nowInSwift, nowInSwift.toKotlinInstant().toNSDate())
32+ }
2633 }
2734
2835 @Test
2936 fun testToFromNSDate () {
30- // The first day on the Gregorian calendar. The day before it is 1582-10-04 in the Julian calendar.
31- val gregorianCalendarStart = Instant .parse(" 1582-10-15T00:00:00Z" ).toEpochMilliseconds()
32- val minBoundMillis = (NSDate .distantPast.timeIntervalSince1970 * 1000 + 0.5 ).toLong()
33- val maxBoundMillis = (NSDate .distantFuture.timeIntervalSince1970 * 1000 - 0.5 ).toLong()
37+ val secondsBound = NSDate .distantPast.timeIntervalSince1970.toLong() until
38+ NSDate .distantFuture.timeIntervalSince1970.toLong()
3439 repeat(STRESS_TEST_ITERATIONS ) {
35- val millis = Random .nextLong(minBoundMillis, maxBoundMillis)
36- val instant = Instant .fromEpochMilliseconds(millis)
37- val date = instant.toNSDate()
38- // Darwin's date printer dynamically adjusts to switching between calendars, while our Instant does not.
39- if (millis >= gregorianCalendarStart) {
40- assertEquals(instant, Instant .parse(dateFormatter.stringFromDate(date)))
41- }
42- assertEquals(instant, date.toKotlinInstant())
40+ val seconds = Random .nextLong(secondsBound)
41+ val nanos = Random .nextInt(0 , NANOS_PER_ONE )
42+ val instant = Instant .fromEpochSeconds(seconds, nanos)
43+ // at most 6 microseconds difference was observed in 10^8 iterations
44+ assertEqualUpToTenMicroseconds(instant, instant.toNSDate().toKotlinInstant())
45+ // while here, no difference at all was observed
46+ assertEqualUpToOneNanosecond(instant.toNSDate(), instant.toNSDate().toKotlinInstant().toNSDate())
4347 }
4448 }
4549
@@ -79,32 +83,60 @@ class ConvertersTest {
7983
8084 @Test
8185 fun localDateToNSDateComponentsTest () {
82- val date = LocalDate .parse( " 2019-02-04 " )
86+ val date = LocalDate ( 2019 , 2 , 4 )
8387 val components = date.toNSDateComponents()
8488 components.timeZone = utc
85- val nsDate = gregorian.dateFromComponents(components)!!
86- val formatter = NSDateFormatter ()
87- formatter.timeZone = utc
88- formatter.dateFormat = " yyyy-MM-dd"
89+ val nsDate = isoCalendar.dateFromComponents(components)!!
90+ val formatter = NSDateFormatter ().apply {
91+ timeZone = utc
92+ dateFormat = " yyyy-MM-dd"
93+ }
8994 assertEquals(" 2019-02-04" , formatter.stringFromDate(nsDate))
9095 }
9196
9297 @Test
9398 fun localDateTimeToNSDateComponentsTest () {
94- val str = " 2019-02-04T23:59:30.543"
95- val dateTime = LocalDateTime .parse(str)
99+ val dateTime = LocalDate (2019 , 2 , 4 ).atTime(23 , 59 , 30 , 123456000 )
96100 val components = dateTime.toNSDateComponents()
97101 components.timeZone = utc
98- val nsDate = gregorian .dateFromComponents(components)!!
99- assertEquals(str + " Z " , dateFormatter.stringFromDate(nsDate ))
102+ val nsDate = isoCalendar .dateFromComponents(components)!!
103+ assertEqualUpToHalfMicrosecond(dateTime.toInstant( TimeZone . UTC ), nsDate.toKotlinInstant( ))
100104 }
101105
102- @OptIn(kotlinx.cinterop. ExperimentalForeignApi ::class )
106+ @OptIn(ExperimentalForeignApi :: class , UnsafeNumber ::class )
103107 private fun zoneOffsetCheck (timeZone : FixedOffsetTimeZone , hours : Int , minutes : Int ) {
104108 val nsTimeZone = timeZone.toNSTimeZone()
105109 val kotlinTimeZone = nsTimeZone.toKotlinTimeZone()
106110 assertEquals(hours * 3600 + minutes * 60 , nsTimeZone.secondsFromGMT.convert())
107111 assertIs<FixedOffsetTimeZone >(kotlinTimeZone)
108112 assertEquals(timeZone.offset, kotlinTimeZone.offset)
109113 }
114+
115+ private fun assertEqualUpToTenMicroseconds (instant1 : Instant , instant2 : Instant ) {
116+ if ((instant1 - instant2).inWholeMicroseconds.absoluteValue > 10 ) {
117+ throw AssertionError (" Expected $instant1 to be equal to $instant2 up to 10 microseconds" )
118+ }
119+ }
120+
121+ private fun assertEqualUpToHalfMicrosecond (instant1 : Instant , instant2 : Instant ) {
122+ if ((instant1 - instant2).inWholeNanoseconds.absoluteValue > 500 ) {
123+ throw AssertionError (" Expected $instant1 to be equal to $instant2 up to 0.5 microseconds" )
124+ }
125+ }
126+
127+ private fun assertEqualUpToHalfMicrosecond (date1 : NSDate , date2 : NSDate ) {
128+ val difference = abs(date1.timeIntervalSinceDate(date2) * 1000000 )
129+ if (difference > 0.5 ) {
130+ throw AssertionError (" Expected $date1 to be equal to $date2 up to 0.5 microseconds, " +
131+ " but the difference was $difference microseconds" )
132+ }
133+ }
134+
135+ private fun assertEqualUpToOneNanosecond (date1 : NSDate , date2 : NSDate ) {
136+ val difference = abs(date1.timeIntervalSinceDate(date2) * 1000000000 )
137+ if (difference > 1 ) {
138+ throw AssertionError (" Expected $date1 to be equal to $date2 up to 1 nanosecond, " +
139+ " but the difference was $difference microseconds" )
140+ }
141+ }
110142}
0 commit comments