cosinekitty / astronomy

Astronomy Engine: multi-language calculation of Sun, Moon, and planet positions. Predicts lunar phases, eclipses, transits, oppositions, conjunctions, equinoxes, solstices, rise/set times, and other events. Provides vector and angular coordinate transforms among equatorial, ecliptic, horizontal, and galactic orientations.
MIT License
483 stars 63 forks source link

Kotlin: investigate non-JVM compatible date/time support #178

Closed cosinekitty closed 2 years ago

cosinekitty commented 2 years ago

@ebraminio wrote:

For being KMM compatible, in addition to a slightly different project structure, we should get rid of these import java.text.SimpleDateFormat (Calendar and Date) and import java.util.* also which I don't know much about the alternatives yet.

In general, it would be nice to provide JVM support via Kotlin, without requiring JVM; Kotlin can be used for other platforms too.

See if it is possible to write the Astronomy Engine core in Kotlin without any Java/JVM dependencies, providing additional sample code for use with Java-specific date/time, outside the core code.

ebraminio commented 2 years ago

It probably is possible to get rid both Kotlin's stdlib https://jakewharton.com/shrinking-a-kotlin-binary/ https://discuss.kotlinlang.org/t/using-kotlin-without-stdlib/19028/2 https://discuss.kotlinlang.org/t/can-you-use-kotlin-libraries-in-java-without-the-kotlin-runtime/24034/3 https://stackoverflow.com/questions/54272746 and Java's dependency using actual/expect system https://kotlinlang.org/docs/multiplatform-connect-to-apis.html#rules-for-expected-and-actual-declarations which we will need the latter sooner or later.

Even in C implementation it is possible to get rid of libc uses which may sound crazy but I actually needed it and have done so for a WebAssembly related thing, borrowing some of libc implementations into my work.

ebraminio commented 2 years ago

I'm looking at each and hopefully will try to settle this before the release :)

cosinekitty commented 2 years ago

I'm looking at the Kotlin compiler release page to try to understand supporting both Kotlin on JVM and Native Kotlin. I have a feeling that I should create a native demo app and make sure Astronomy Engine works correctly with native LLVM generated code also. Does it make sense to use the command line compilers from the above page to do this?

For my own use, it would be nice to be able to create native binaries from the Kotlin code. Perhaps using command line compilers will be easier than trying to understand Gradle configurations, etc.

ebraminio commented 2 years ago

I have a feeling that I should create a native demo app and make sure Astronomy Engine works correctly with native LLVM generated code also.

Makes sense.

Does it make sense to use the command line compilers from the above page to do this?

I've just tested it very briefly so far tbh. I think first we should find a similar computation library with KMM support. I found https://github.com/AAkira/Kotlin-Multiplatform-Libraries and found https://github.com/ajalt/clikt/tree/master/clikt interesting but I'm yet myself to learn to see how this works at all

cosinekitty commented 2 years ago

That is interesting stuff. Yes, clikt looks interesting for generating nice command line utilities. There are things like that for Python also, where it parses command line options, etc. For Astronomy Engine demos, I would rather keep things super simple, with minimum dependency on other libraries. for Astronomy Engine core, I doubt we will need anything OS-specific to even worry about. The only thing would be date/time support, which we can show a couple of one-line functions to translate back and forth between target-specific types like GregorianCalendar. I will try to add something simple that runs in GitHub Actions.

cosinekitty commented 2 years ago

I have eliminated the Java-dependent date/time classes in this commit: 61e398b59256c0286902e82128a0a7f88a43ea6f.

@ebraminio: this may be a breaking change for your Android app. However, I have provided new methods that should make it easy to convert Java date/time back and forth to Astronomy Engine Time objects.

ebraminio commented 2 years ago

Fantastic.

this may be a breaking change for your Android app

Don't mind that at all, now we just need someway to tell gradle we don't have JVM dependency. Guess these lines are related https://github.com/ajalt/clikt/blob/master/clikt/build.gradle.kts#L11 https://github.com/ajalt/clikt/blob/master/clikt/build.gradle.kts#L29-L37 but I should look at it.

cosinekitty commented 2 years ago

OK, and meanwhile I am trying to get a native demo program to build, but I am not having much luck so far:

don@spearmint:~/github/astronomy/demo/kotlin/native $ kotlinc-native moonphase.kt ../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt -o moonphase
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:27:7: error: unresolved reference: JvmName
@file:JvmName("Astronomy")
      ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:421:20: error: unresolved reference: format
            "%04d".format(year) +
                   ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:423:20: error: unresolved reference: format
            "%02d".format(month) +
                   ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:425:20: error: unresolved reference: format
            "%02d".format(day) +
                   ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:427:20: error: unresolved reference: format
            "%02d".format(hour) +
                   ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:429:20: error: unresolved reference: format
            "%02d".format(minute) +
                   ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:431:20: error: unresolved reference: format
            "%02d".format(wholeSeconds) +
                   ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:433:20: error: unresolved reference: format
            "%03d".format(wholeMillis) +
                   ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:596:10: error: unresolved reference: JvmStatic
        @JvmStatic
         ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:607:10: error: unresolved reference: JvmStatic
        @JvmStatic
         ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:657:10: error: unresolved reference: JvmStatic
        @JvmStatic
         ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:1115:10: error: unresolved reference: JvmStatic
        @JvmStatic
         ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:3043:5: error: unresolved reference: synchronized
    synchronized (plutoCache) {
    ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:3089:9: error: 'return' is not allowed here
        return list
        ^
/home/don/github/astronomy/demo/kotlin/native/../../../source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt:3091:1: error: a 'return' expression required in a function with a block body ('{...}')
}
^
moonphase.kt:3:30: error: packages cannot be imported
import io.github.cosinekitty.astronomy
                             ^
moonphase.kt:7:38: error: unresolved reference: Time
private fun parseDate(text: String): Time {
                                     ^
moonphase.kt:21:12: error: unresolved reference: Time
    return Time(year, month, day, hour, minute, second)
           ^
ebraminio commented 2 years ago

OK, and meanwhile I am trying to get a native demo program to build, but I am not having much luck so far:

Trying to make it work from command line was a nice idea :) Of all that the synchronized one is more concerning to me as https://stackoverflow.com/a/56883136 , and I've seen why you've used it, am a bit out of idea here but let's see.

ebraminio commented 2 years ago

Worst case you'll declare codes that alter that part of Time object non thread safe in non JVM, no need to concern more than that and hopefully that syntax can be poly-filled.

cosinekitty commented 2 years ago

The synchronized is used to protect a lookup table for calculating the orbit of Pluto. Take a look at this part:

private fun getPlutoSegment(tt: Double): List<BodyGravCalc>? {
    if (tt < plutoStateTable[0].tt || tt > plutoStateTable[PLUTO_NUM_STATES-1].tt)
        return null     // Don't bother calculating a segment. Let the caller crawl backward/forward to this time

    val segIndex = clampIndex((tt - plutoStateTable[0].tt) / PLUTO_TIME_STEP, PLUTO_NUM_STATES-1)
    synchronized (plutoCache) {
        // ...

I will soon push the code that includes the Kotlin Native source moonphase.kt whose build is broken. Can you figure out how to get it to build and run? For now, I just want it to create a Time object and print the string representation to the screen. If we can get that working, I can make it actually calculate moon phases later.

ebraminio commented 2 years ago

Sure, I'd say try to make it work on kotlin-jvm, it should be possible to invoke that from command line as well till I figure out what to do here :)

ebraminio commented 2 years ago

Ok just reached to this, wasted some unproductive times with gradle with no much luck, but at least have made what you wanted compile, those Jvm thing aren't needed for my use but are somehow needed for a Java language support, the common practice is to have a separate file to add them but I really don't know tbh,

diff --git a/demo/kotlin/native/moonphase.kt b/demo/kotlin/native/moonphase.kt
index bc546e5c..3a4ea1c4 100644
--- a/demo/kotlin/native/moonphase.kt
+++ b/demo/kotlin/native/moonphase.kt
@@ -1,5 +1,5 @@
 import kotlin.system.exitProcess
-import io.github.cosinekitty.astronomy
+import io.github.cosinekitty.astronomy.Time

 fun main() {
     val time = Time(2019, 6, 15, 9, 15, 32.987)
diff --git a/generate/template/astronomy.kt b/generate/template/astronomy.kt
index daf3fc9f..29736b65 100644
--- a/generate/template/astronomy.kt
+++ b/generate/template/astronomy.kt
@@ -24,7 +24,7 @@
     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     SOFTWARE.
 */
-@file:JvmName("Astronomy")
+//@file:JvmName("Astronomy")

 package io.github.cosinekitty.astronomy

@@ -418,7 +418,7 @@ class DateTime(
         val wholeMillis: Int = millis.toInt()

         return (
-            "%04d".format(year) +
+          /*  "%04d".format(year) +
             "-" +
             "%02d".format(month) +
             "-" +
@@ -430,7 +430,7 @@ class DateTime(
             ":" +
             "%02d".format(wholeSeconds) +
             "." +
-            "%03d".format(wholeMillis) +
+            "%03d".format(wholeMillis) +*/
             "Z"
         )
     }
diff --git a/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt b/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt
index 59b6861e..3ec1d319 100644
--- a/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt
+++ b/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt
@@ -24,7 +24,7 @@
     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     SOFTWARE.
 */
-@file:JvmName("Astronomy")
+//@file:JvmName("Astronomy")

 package io.github.cosinekitty.astronomy

@@ -418,7 +418,7 @@ class DateTime(
         val wholeMillis: Int = millis.toInt()

         return (
-            "%04d".format(year) +
+           /* "%04d".format(year) +
             "-" +
             "%02d".format(month) +
             "-" +
@@ -430,7 +430,7 @@ class DateTime(
             ":" +
             "%02d".format(wholeSeconds) +
             "." +
-            "%03d".format(wholeMillis) +
+            "%03d".format(wholeMillis) +*/
             "Z"
         )
     }
@@ -593,7 +593,7 @@ class Time private constructor(
          *
          * @param tt The number of days after the J2000 epoch.
          */
-        @JvmStatic
+//        @JvmStatic
         fun fromTerrestrialTime(tt: Double) = Time(universalTime(tt), tt)

         /**
@@ -604,7 +604,7 @@ class Time private constructor(
          * To facilitate using such values for astronomy calculations, this
          * function converts a millsecond count into a `Time` object.
          */
-        @JvmStatic
+       // @JvmStatic
         fun fromMillisecondsSince1970(millis: Long) = Time((millis - 946728000000L) / MILLISECONDS_PER_DAY)
     }
 }
@@ -654,7 +654,7 @@ internal data class TerseVector(var x: Double, var y: Double, var z: Double) {
     }

     companion object {
-        @JvmStatic
+        //@JvmStatic
         fun zero() = TerseVector(0.0, 0.0, 0.0)
     }
 }
@@ -1112,7 +1112,7 @@ class RotationMatrix(
          * This matrix can be the starting point for other operations,
          * such as calling a series of [RotationMatrix.combine] or [RotationMatrix.pivot].
          */
-        @JvmStatic
+        //@JvmStatic
         fun identity() = RotationMatrix(
             1.0, 0.0, 0.0,
             0.0, 1.0, 0.0,
@@ -3040,7 +3040,7 @@ private fun getPlutoSegment(tt: Double): List<BodyGravCalc>? {
         return null     // Don't bother calculating a segment. Let the caller crawl backward/forward to this time

     val segIndex = clampIndex((tt - plutoStateTable[0].tt) / PLUTO_TIME_STEP, PLUTO_NUM_STATES-1)
-    synchronized (plutoCache) {
+    //synchronized (plutoCache) {
         var list = plutoCache.get(segIndex)
         if (list == null) {
             val seg = ArrayList<BodyGravCalc>()
@@ -3087,7 +3087,7 @@ private fun getPlutoSegment(tt: Double): List<BodyGravCalc>? {
             plutoCache.set(segIndex, list)
         }
         return list
-    }
+    //}
 }

 private fun calcPlutoOneWay(

what we should do eventually is I guess to create a KMM project with the IDE using https://kotlinlang.org/docs/multiplatform-create-lib.html move what we have right now to it, hopefully that make the things to work.

cosinekitty commented 2 years ago

OK, I think for now we will postpone the effort to use the Kotlin Native compiler. I think supporting JVM is much more important than supporting native executables with Kotlin, because C and C# can already make native executables. The JVM support is a big missing piece, so that takes higher priority. Do you agree with that, or am I forgetting something?

ebraminio commented 2 years ago

Specially after removal of reliance to java.util.*, things are very good here actually as we know the interface, though became a little more harder to use which can be fixed later, doesn't need to have breaking changes at least so I think we can go with this for now.