| 
1 | 1 | package org.jetbrains.kotlinx.dataframe.api  | 
2 | 2 | 
 
  | 
 | 3 | +import io.kotest.matchers.should  | 
3 | 4 | import io.kotest.matchers.shouldBe  | 
 | 5 | +import kotlinx.datetime.DateTimeUnit  | 
4 | 6 | import kotlinx.datetime.Instant  | 
5 | 7 | import kotlinx.datetime.LocalDate  | 
6 | 8 | import kotlinx.datetime.LocalDateTime  | 
7 | 9 | import kotlinx.datetime.LocalTime  | 
8 | 10 | import kotlinx.datetime.Month  | 
 | 11 | +import kotlinx.datetime.format.DateTimeComponents  | 
 | 12 | +import kotlinx.datetime.plus  | 
 | 13 | +import kotlinx.datetime.toJavaInstant  | 
 | 14 | +import kotlinx.datetime.toKotlinInstant  | 
9 | 15 | import org.jetbrains.kotlinx.dataframe.DataFrame  | 
 | 16 | +import org.jetbrains.kotlinx.dataframe.impl.api.Parsers  | 
 | 17 | +import org.jetbrains.kotlinx.dataframe.impl.catchSilent  | 
10 | 18 | import org.jetbrains.kotlinx.dataframe.type  | 
11 | 19 | import org.junit.Test  | 
12 | 20 | import java.util.Locale  | 
 | 21 | +import kotlin.random.Random  | 
13 | 22 | import kotlin.reflect.typeOf  | 
 | 23 | +import kotlin.time.Duration  | 
14 | 24 | import kotlin.time.Duration.Companion.days  | 
15 | 25 | import kotlin.time.Duration.Companion.hours  | 
 | 26 | +import kotlin.time.Duration.Companion.microseconds  | 
 | 27 | +import kotlin.time.Duration.Companion.milliseconds  | 
16 | 28 | import kotlin.time.Duration.Companion.minutes  | 
 | 29 | +import kotlin.time.Duration.Companion.nanoseconds  | 
17 | 30 | import kotlin.time.Duration.Companion.seconds  | 
 | 31 | +import java.time.Duration as JavaDuration  | 
 | 32 | +import java.time.Instant as JavaInstant  | 
18 | 33 | 
 
  | 
19 | 34 | class ParseTests {  | 
20 | 35 |  @Test  | 
@@ -142,9 +157,340 @@ class ParseTests {  | 
142 | 157 |  columnOf("2022-01-23T04:29:40").parse().type shouldBe typeOf<LocalDateTime>()  | 
143 | 158 |  }  | 
144 | 159 | 
 
  | 
 | 160 | + @Test  | 
 | 161 | + fun `can parse instants`() {  | 
 | 162 | + val instantParser = Parsers[typeOf<Instant>()]!!.applyOptions(null)  | 
 | 163 | + val javaInstantParser = Parsers[typeOf<JavaInstant>()]!!.applyOptions(null)  | 
 | 164 | + | 
 | 165 | + // from the kotlinx-datetime tests, java instants treat leap seconds etc. like this  | 
 | 166 | + fun parseInstantLikeJavaDoesOrNull(input: String): Instant? =  | 
 | 167 | + catchSilent {  | 
 | 168 | + DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parseOrNull(input)?.apply {  | 
 | 169 | + when {  | 
 | 170 | + hour == 24 && minute == 0 && second == 0 && nanosecond == 0 -> {  | 
 | 171 | + setDate(toLocalDate().plus(1, DateTimeUnit.DAY))  | 
 | 172 | + hour = 0  | 
 | 173 | + }  | 
 | 174 | + | 
 | 175 | + hour == 23 && minute == 59 && second == 60 -> second = 59  | 
 | 176 | + }  | 
 | 177 | + }?.toInstantUsingOffset()  | 
 | 178 | + }  | 
 | 179 | + | 
 | 180 | + fun formatTwoDigits(i: Int) = if (i < 10) "0$i" else "$i"  | 
 | 181 | + | 
 | 182 | + for (hour in 23..25) {  | 
 | 183 | + for (minute in listOf(0..5, 58..62).flatten()) {  | 
 | 184 | + for (second in listOf(0..5, 58..62).flatten()) {  | 
 | 185 | + val input = "2020-03-16T$hour:${formatTwoDigits(minute)}:${formatTwoDigits(second)}Z"  | 
 | 186 | + | 
 | 187 | + val myParserRes = instantParser(input) as Instant?  | 
 | 188 | + val myJavaParserRes = javaInstantParser(input) as JavaInstant?  | 
 | 189 | + val instantRes = catchSilent { Instant.parse(input) }  | 
 | 190 | + val instantLikeJava = parseInstantLikeJavaDoesOrNull(input)  | 
 | 191 | + val javaInstantRes = catchSilent { JavaInstant.parse(input) }  | 
 | 192 | + | 
 | 193 | + // our parser has a fallback mechanism built in, like this  | 
 | 194 | + myParserRes shouldBe (instantRes ?: javaInstantRes?.toKotlinInstant())  | 
 | 195 | + myParserRes shouldBe instantLikeJava  | 
 | 196 | + | 
 | 197 | + myJavaParserRes shouldBe javaInstantRes  | 
 | 198 | + | 
 | 199 | + myParserRes?.toJavaInstant() shouldBe instantLikeJava?.toJavaInstant()  | 
 | 200 | + instantLikeJava?.toJavaInstant() shouldBe myJavaParserRes  | 
 | 201 | + myJavaParserRes shouldBe javaInstantRes  | 
 | 202 | + }  | 
 | 203 | + }  | 
 | 204 | + }  | 
 | 205 | + }  | 
 | 206 | + | 
145 | 207 |  @Test  | 
146 | 208 |  fun `parse duration`() {  | 
147 | 209 |  columnOf("1d 15m", "20h 35m 11s").parse() shouldBe  | 
148 | 210 |  columnOf(1.days + 15.minutes, 20.hours + 35.minutes + 11.seconds)  | 
149 | 211 |  }  | 
 | 212 | + | 
 | 213 | + @Test  | 
 | 214 | + fun `can parse duration isoStrings`() {  | 
 | 215 | + val durationParser = Parsers[typeOf<Duration>()]!!.applyOptions(null) as (String) -> Duration?  | 
 | 216 | + val javaDurationParser = Parsers[typeOf<JavaDuration>()]!!.applyOptions(null) as (String) -> JavaDuration?  | 
 | 217 | + | 
 | 218 | + fun testSuccess(duration: Duration, vararg isoStrings: String) {  | 
 | 219 | + isoStrings.first() shouldBe duration.toIsoString()  | 
 | 220 | + for (isoString in isoStrings) {  | 
 | 221 | + Duration.parse(isoString) shouldBe duration  | 
 | 222 | + durationParser(isoString) shouldBe duration  | 
 | 223 | + | 
 | 224 | + javaDurationParser(isoString) shouldBe catchSilent { JavaDuration.parse(isoString) }  | 
 | 225 | + }  | 
 | 226 | + }  | 
 | 227 | + | 
 | 228 | + // zero  | 
 | 229 | + testSuccess(Duration.ZERO, "PT0S", "P0D", "PT0H", "PT0M", "P0DT0H", "PT0H0M", "PT0H0S")  | 
 | 230 | + | 
 | 231 | + // single unit  | 
 | 232 | + testSuccess(1.days, "PT24H", "P1D", "PT1440M", "PT86400S")  | 
 | 233 | + testSuccess(1.hours, "PT1H")  | 
 | 234 | + testSuccess(1.minutes, "PT1M")  | 
 | 235 | + testSuccess(1.seconds, "PT1S")  | 
 | 236 | + testSuccess(1.milliseconds, "PT0.001S")  | 
 | 237 | + testSuccess(1.microseconds, "PT0.000001S")  | 
 | 238 | + testSuccess(1.nanoseconds, "PT0.000000001S", "PT0.0000000009S")  | 
 | 239 | + testSuccess(0.9.nanoseconds, "PT0.000000001S")  | 
 | 240 | + | 
 | 241 | + // rounded to zero  | 
 | 242 | + testSuccess(0.1.nanoseconds, "PT0S")  | 
 | 243 | + testSuccess(Duration.ZERO, "PT0S", "PT0.0000000004S")  | 
 | 244 | + | 
 | 245 | + // several units combined  | 
 | 246 | + testSuccess(1.days + 1.minutes, "PT24H1M")  | 
 | 247 | + testSuccess(1.days + 1.seconds, "PT24H0M1S")  | 
 | 248 | + testSuccess(1.days + 1.milliseconds, "PT24H0M0.001S")  | 
 | 249 | + testSuccess(1.hours + 30.minutes, "PT1H30M")  | 
 | 250 | + testSuccess(1.hours + 500.milliseconds, "PT1H0M0.500S")  | 
 | 251 | + testSuccess(2.minutes + 500.milliseconds, "PT2M0.500S")  | 
 | 252 | + testSuccess(90_500.milliseconds, "PT1M30.500S")  | 
 | 253 | + | 
 | 254 | + // with sign  | 
 | 255 | + testSuccess(-1.days + 15.minutes, "-PT23H45M", "PT-23H-45M", "+PT-24H+15M")  | 
 | 256 | + testSuccess(-1.days - 15.minutes, "-PT24H15M", "PT-24H-15M", "-PT25H-45M")  | 
 | 257 | + testSuccess(Duration.ZERO, "PT0S", "P1DT-24H", "+PT-1H+60M", "-PT1M-60S")  | 
 | 258 | + | 
 | 259 | + // infinite  | 
 | 260 | + testSuccess(  | 
 | 261 | + Duration.INFINITE,  | 
 | 262 | + "PT9999999999999H",  | 
 | 263 | + "PT+10000000000000H",  | 
 | 264 | + "-PT-9999999999999H",  | 
 | 265 | + "-PT-1234567890123456789012S",  | 
 | 266 | + )  | 
 | 267 | + testSuccess(-Duration.INFINITE, "-PT9999999999999H", "-PT10000000000000H", "PT-1234567890123456789012S")  | 
 | 268 | + | 
 | 269 | + fun testFailure(isoString: String) {  | 
 | 270 | + catchSilent { Duration.parse(isoString) } shouldBe durationParser(isoString)  | 
 | 271 | + catchSilent { JavaDuration.parse(isoString) } shouldBe javaDurationParser(isoString)  | 
 | 272 | + }  | 
 | 273 | + | 
 | 274 | + listOf(  | 
 | 275 | + "",  | 
 | 276 | + " ",  | 
 | 277 | + "P",  | 
 | 278 | + "PT",  | 
 | 279 | + "P1DT",  | 
 | 280 | + "P1",  | 
 | 281 | + "PT1",  | 
 | 282 | + "0",  | 
 | 283 | + "+P",  | 
 | 284 | + "+",  | 
 | 285 | + "-",  | 
 | 286 | + "h",  | 
 | 287 | + "H",  | 
 | 288 | + "something",  | 
 | 289 | + "1m",  | 
 | 290 | + "1d",  | 
 | 291 | + "2d 11s",  | 
 | 292 | + "Infinity",  | 
 | 293 | + "-Infinity", // successful in kotlin, not in java  | 
 | 294 | + "P+12+34D",  | 
 | 295 | + "P12-34D",  | 
 | 296 | + "PT1234567890-1234567890S",  | 
 | 297 | + " P1D",  | 
 | 298 | + "PT1S ",  | 
 | 299 | + "P3W",  | 
 | 300 | + "P1Y",  | 
 | 301 | + "P1M",  | 
 | 302 | + "P1S",  | 
 | 303 | + "PT1D",  | 
 | 304 | + "PT1Y",  | 
 | 305 | + "PT1S2S",  | 
 | 306 | + "PT1S2H",  | 
 | 307 | + "P9999999999999DT-9999999999999H",  | 
 | 308 | + "PT1.5H",  | 
 | 309 | + "PT0.5D",  | 
 | 310 | + "PT.5S",  | 
 | 311 | + "PT0.25.25S",  | 
 | 312 | + ).forEach(::testFailure)  | 
 | 313 | + }  | 
 | 314 | + | 
 | 315 | + @Test  | 
 | 316 | + fun `can parse duration default kotlin strings`() {  | 
 | 317 | + val durationParser = Parsers[typeOf<Duration>()]!!.applyOptions(null) as (String) -> Duration?  | 
 | 318 | + | 
 | 319 | + fun testParsing(string: String, expectedDuration: Duration) {  | 
 | 320 | + Duration.parse(string) shouldBe expectedDuration  | 
 | 321 | + durationParser(string) shouldBe expectedDuration  | 
 | 322 | + }  | 
 | 323 | + | 
 | 324 | + fun testSuccess(duration: Duration, vararg expected: String) {  | 
 | 325 | + val actual = duration.toString()  | 
 | 326 | + actual shouldBe expected.first()  | 
 | 327 | + | 
 | 328 | + if (duration.isPositive()) {  | 
 | 329 | + if (' ' in actual) {  | 
 | 330 | + (-duration).toString() shouldBe "-($actual)"  | 
 | 331 | + } else {  | 
 | 332 | + (-duration).toString() shouldBe "-$actual"  | 
 | 333 | + }  | 
 | 334 | + }  | 
 | 335 | + | 
 | 336 | + for (string in expected) {  | 
 | 337 | + testParsing(string, duration)  | 
 | 338 | + if (duration.isPositive() && duration.isFinite()) {  | 
 | 339 | + testParsing("+($string)", duration)  | 
 | 340 | + testParsing("-($string)", -duration)  | 
 | 341 | + if (' ' !in string) {  | 
 | 342 | + testParsing("+$string", duration)  | 
 | 343 | + testParsing("-$string", -duration)  | 
 | 344 | + }  | 
 | 345 | + }  | 
 | 346 | + }  | 
 | 347 | + }  | 
 | 348 | + | 
 | 349 | + testSuccess(101.days, "101d", "2424h")  | 
 | 350 | + testSuccess(45.3.days, "45d 7h 12m", "45.3d", "45d 7.2h") // 0.3d == 7.2h  | 
 | 351 | + testSuccess(45.days, "45d")  | 
 | 352 | + | 
 | 353 | + testSuccess(40.5.days, "40d 12h", "40.5d", "40d 720m")  | 
 | 354 | + testSuccess(40.days + 20.minutes, "40d 0h 20m", "40d 20m", "40d 1200s")  | 
 | 355 | + testSuccess(40.days + 20.seconds, "40d 0h 0m 20s", "40d 20s")  | 
 | 356 | + testSuccess(40.days + 100.nanoseconds, "40d 0h 0m 0.000000100s", "40d 100ns")  | 
 | 357 | + | 
 | 358 | + testSuccess(40.hours + 15.minutes, "1d 16h 15m", "40h 15m")  | 
 | 359 | + testSuccess(40.hours, "1d 16h", "40h")  | 
 | 360 | + | 
 | 361 | + testSuccess(12.5.hours, "12h 30m")  | 
 | 362 | + testSuccess(12.hours + 15.seconds, "12h 0m 15s")  | 
 | 363 | + testSuccess(12.hours + 1.nanoseconds, "12h 0m 0.000000001s")  | 
 | 364 | + testSuccess(30.minutes, "30m")  | 
 | 365 | + testSuccess(17.5.minutes, "17m 30s")  | 
 | 366 | + | 
 | 367 | + testSuccess(16.5.minutes, "16m 30s")  | 
 | 368 | + testSuccess(1097.1.seconds, "18m 17.1s")  | 
 | 369 | + testSuccess(90.36.seconds, "1m 30.36s")  | 
 | 370 | + testSuccess(50.seconds, "50s")  | 
 | 371 | + testSuccess(1.3.seconds, "1.3s")  | 
 | 372 | + testSuccess(1.seconds, "1s")  | 
 | 373 | + | 
 | 374 | + testSuccess(0.5.seconds, "500ms")  | 
 | 375 | + testSuccess(40.2.milliseconds, "40.2ms")  | 
 | 376 | + testSuccess(4.225.milliseconds, "4.225ms")  | 
 | 377 | + testSuccess(4.24501.milliseconds, "4.245010ms", "4ms 245us 10ns")  | 
 | 378 | + testSuccess(1.milliseconds, "1ms")  | 
 | 379 | + | 
 | 380 | + testSuccess(0.75.milliseconds, "750us")  | 
 | 381 | + testSuccess(75.35.microseconds, "75.35us")  | 
 | 382 | + testSuccess(7.25.microseconds, "7.25us")  | 
 | 383 | + testSuccess(1.035.microseconds, "1.035us")  | 
 | 384 | + testSuccess(1.005.microseconds, "1.005us")  | 
 | 385 | + testSuccess(1800.nanoseconds, "1.8us", "1800ns", "0.0000000005h")  | 
 | 386 | + | 
 | 387 | + testSuccess(950.5.nanoseconds, "951ns")  | 
 | 388 | + testSuccess(85.23.nanoseconds, "85ns")  | 
 | 389 | + testSuccess(8.235.nanoseconds, "8ns")  | 
 | 390 | + testSuccess(1.nanoseconds, "1ns", "0.9ns", "0.001us", "0.0009us")  | 
 | 391 | + testSuccess(1.3.nanoseconds, "1ns")  | 
 | 392 | + testSuccess(0.75.nanoseconds, "1ns")  | 
 | 393 | + testSuccess(0.7512.nanoseconds, "1ns")  | 
 | 394 | + | 
 | 395 | + // equal to zero  | 
 | 396 | +// testSuccess(0.023.nanoseconds, "0.023ns")  | 
 | 397 | +// testSuccess(0.0034.nanoseconds, "0.0034ns")  | 
 | 398 | +// testSuccess(0.0000035.nanoseconds, "0.0000035ns")  | 
 | 399 | + | 
 | 400 | + testSuccess(Duration.ZERO, "0s", "0.4ns", "0000.0000ns")  | 
 | 401 | + testSuccess(365.days * 10000, "3650000d")  | 
 | 402 | + testSuccess(300.days * 100000, "30000000d")  | 
 | 403 | + testSuccess(365.days * 100000, "36500000d")  | 
 | 404 | + testSuccess(((Long.MAX_VALUE / 2) - 1).milliseconds, "53375995583d 15h 36m 27.902s") // max finite value  | 
 | 405 | + | 
 | 406 | + // all infinite  | 
 | 407 | +// val universeAge = Duration.days(365.25) * 13.799e9  | 
 | 408 | +// val planckTime = Duration.seconds(5.4e-44)  | 
 | 409 | + | 
 | 410 | +// testSuccess(universeAge, "5.04e+12d")  | 
 | 411 | +// testSuccess(planckTime, "5.40e-44s")  | 
 | 412 | +// testSuccess(Duration.nanoseconds(Double.MAX_VALUE), "2.08e+294d")  | 
 | 413 | + testSuccess(Duration.INFINITE, "Infinity", "53375995583d 20h", "+Infinity")  | 
 | 414 | + testSuccess(-Duration.INFINITE, "-Infinity", "-(53375995583d 20h)")  | 
 | 415 | + | 
 | 416 | + fun testFailure(isoString: String) {  | 
 | 417 | + catchSilent { Duration.parse(isoString) } shouldBe durationParser(isoString)  | 
 | 418 | + }  | 
 | 419 | + | 
 | 420 | + listOf(  | 
 | 421 | + "",  | 
 | 422 | + " ",  | 
 | 423 | + "P",  | 
 | 424 | + "PT",  | 
 | 425 | + "P1DT",  | 
 | 426 | + "P1",  | 
 | 427 | + "PT1",  | 
 | 428 | + "0",  | 
 | 429 | + "+P",  | 
 | 430 | + "+",  | 
 | 431 | + "-",  | 
 | 432 | + "h",  | 
 | 433 | + "H",  | 
 | 434 | + "something",  | 
 | 435 | + "1234567890123456789012ns",  | 
 | 436 | + "Inf",  | 
 | 437 | + "-Infinity value",  | 
 | 438 | + "1s ",  | 
 | 439 | + " 1s",  | 
 | 440 | + "1d 1m 1h",  | 
 | 441 | + "1s 2s",  | 
 | 442 | + "-12m 15s",  | 
 | 443 | + "-12m -15s",  | 
 | 444 | + "-()",  | 
 | 445 | + "-(12m 30s",  | 
 | 446 | + "+12m 15s",  | 
 | 447 | + "+12m +15s",  | 
 | 448 | + "+()",  | 
 | 449 | + "+(12m 30s",  | 
 | 450 | + "()",  | 
 | 451 | + "(12m 30s)",  | 
 | 452 | + "12.5m 11.5s",  | 
 | 453 | + ".2s",  | 
 | 454 | + "0.1553.39m",  | 
 | 455 | + "P+12+34D",  | 
 | 456 | + "P12-34D",  | 
 | 457 | + "PT1234567890-1234567890S",  | 
 | 458 | + " P1D",  | 
 | 459 | + "PT1S ",  | 
 | 460 | + "P1Y",  | 
 | 461 | + "P1M",  | 
 | 462 | + "P1S",  | 
 | 463 | + "PT1D",  | 
 | 464 | + "PT1Y",  | 
 | 465 | + "PT1S2S",  | 
 | 466 | + "PT1S2H",  | 
 | 467 | + "P9999999999999DT-9999999999999H",  | 
 | 468 | + "PT1.5H",  | 
 | 469 | + "PT0.5D",  | 
 | 470 | + "PT.5S",  | 
 | 471 | + "PT0.25.25S",  | 
 | 472 | + ).forEach(::testFailure)  | 
 | 473 | + }  | 
 | 474 | + | 
 | 475 | + @Test  | 
 | 476 | + fun `Parse normal string column`() {  | 
 | 477 | + val df = dataFrameOf(List(5_000) { "_$it" }).fill(100) {  | 
 | 478 | + Random.nextInt().toChar().toString() + Random.nextInt().toChar()  | 
 | 479 | + }  | 
 | 480 | + | 
 | 481 | + df.parse()  | 
 | 482 | + }  | 
 | 483 | + | 
 | 484 | + /**  | 
 | 485 | + * Asserts that all elements of the iterable are equal to each other  | 
 | 486 | + */  | 
 | 487 | + private fun <T> Iterable<T>.shouldAllBeEqual(): Iterable<T> {  | 
 | 488 | + this should {  | 
 | 489 | + it.reduce { a, b ->  | 
 | 490 | + a shouldBe b  | 
 | 491 | + b  | 
 | 492 | + }  | 
 | 493 | + }  | 
 | 494 | + return this  | 
 | 495 | + }  | 
150 | 496 | }  | 
0 commit comments