Generate SSD replacement timestamps from typed Jerboa

ober

37ff1a2ecfecd56a4371e24c7428b67f12077375

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index fbb8dda..92761d2 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -15538,6 +15538,9 @@
                       (java nio file LinkOption)
                       (java nio file Path)
                       (java security MessageDigest)
+                      (java time Instant)
+                      (java time OffsetDateTime)
+                      (java time format DateTimeFormatter)
                       (javax net ssl HttpsURLConnection)
                       (org json JSONArray)
                       (org json JSONObject))
@@ -15558,16 +15561,23 @@
                 truthStoreContainedImportPathLocal
                 truthStoreValidateImportedTextLocal
                 truthStoreValidateImportedFileLocal
-                truthStoreAppendJsonLineLocal)
+                truthStoreAppendJsonLineLocal
+                truthStoreTruthTimeLocal
+                truthStoreShouldReplaceLocal)
         (type AssetManager)
+        (type Any)
         (type Context)
+        (type DateTimeFormatter)
         (type Exception)
         (type File)
+        (type Instant)
         (type InputStream)
         (type Int32)
         (type LinkOption)
         (type HttpsURLConnection)
         (type MessageDigest)
+        (type Number)
+        (type OffsetDateTime)
         (type Path)
         (type RemoteConfig)
         (type SsdSession)
@@ -15631,12 +15641,6 @@
                   (destination : File)
                   (text : String)) : Unit
           (kotlin-member-call atomicWriteText))
-        (extern (truthStoreShouldReplaceLocalRaw
-                  (store : TruthStore)
-                  (local : File)
-                  (remoteText : String)
-                  (remoteModified : Int)) : Bool
-          (kotlin-member-call shouldReplaceLocal))
         (extern (truthStoreJsonToPrettyString
                   (json : JSONObject)
                   (indent : Int32)) : String
@@ -15644,6 +15648,34 @@
         (extern (truthStoreJsonToString
                   (json : JSONObject)) : String
           (kotlin-member-call toString))
+        (extern (truthStoreAnyAsNumber
+                  (value : Any)) : (Nullable Number)
+          (kotlin-safe-cast Number))
+        (extern (truthStoreAnyAsString
+                  (value : Any)) : (Nullable String)
+          (kotlin-safe-cast String))
+        (extern (truthStoreNumberToLong
+                  (value : Number)) : Int
+          (kotlin-member-call toLong))
+        (extern (truthStoreStringToLongOrNull
+                  (value : String)) : (Nullable Int)
+          (kotlin-member-call toLongOrNull))
+        (extern (truthStoreDateTimeFormatterPattern
+                  (pattern : String)) : DateTimeFormatter
+          (kotlin-call DateTimeFormatter ofPattern))
+        (extern (truthStoreOffsetDateTimeParse
+                  (value : String)) : OffsetDateTime
+          (kotlin-call OffsetDateTime parse))
+        (extern (truthStoreOffsetDateTimeParseWithFormatter
+                  (value : String)
+                  (formatter : DateTimeFormatter)) : OffsetDateTime
+          (kotlin-call OffsetDateTime parse))
+        (extern (truthStoreOffsetDateTimeToInstant
+                  (value : OffsetDateTime)) : Instant
+          (kotlin-member-call toInstant))
+        (extern (truthStoreInstantToEpochMilli
+                  (value : Instant)) : Int
+          (kotlin-member-call toEpochMilli))
         (extern (truthStoreConnectionInputStream
                   (connection : HttpsURLConnection)) : InputStream
           (kotlin-member-get inputStream))
@@ -15678,6 +15710,9 @@
         (extern (truthStoreFileInputStream
                   (file : File)) : InputStream
           (kotlin-member-call inputStream))
+        (extern (truthStoreFileLastModified
+                  (file : File)) : Int
+          (kotlin-member-call lastModified))
         (extern (truthStoreInputStreamClose
                   (input : InputStream)) : Unit
           (kotlin-member-call close))
@@ -16122,8 +16157,7 @@
                                 compareMoreComplete
                                 (truthGroupCount truth)
                                 (nullableTruthGroupCount local)
-                                (truthStoreShouldReplaceLocalRaw
-                                  store
+                                (truthStoreShouldReplaceLocal
                                   dest
                                   text
                                   modified))))
@@ -16237,7 +16271,79 @@
                     next
                     (truthStoreMaxZipEntryBytes))
                   "Event log quota exceeded")
-                (truthStoreAtomicWriteTextRaw store file next)))))))
+                (truthStoreAtomicWriteTextRaw store file next)))))
+        (def (truthStoreParseTimestampWithPattern
+               (value : String)) : (Nullable Int)
+          (try
+            (nullable-some
+              (truthStoreInstantToEpochMilli
+                (truthStoreOffsetDateTimeToInstant
+                  (truthStoreOffsetDateTimeParseWithFormatter
+                    value
+                    (truthStoreDateTimeFormatterPattern
+                      "yyyy-MM-dd'T'HH:mm:ssZ")))))
+            (catch (error : Exception)
+              (nullable-none Int))))
+        (def (truthStoreParseTimestampIso
+               (value : String)) : (Nullable Int)
+          (try
+            (nullable-some
+              (truthStoreInstantToEpochMilli
+                (truthStoreOffsetDateTimeToInstant
+                  (truthStoreOffsetDateTimeParse value))))
+            (catch (error : Exception)
+              (nullable-none Int))))
+        (def (truthStoreParseTimestampText
+               (value : String)) : (Nullable Int)
+          (let ((numeric (truthStoreStringToLongOrNull value)))
+            (if (not (nullable-null? numeric))
+              numeric
+              (let ((pattern
+                      (truthStoreParseTimestampWithPattern value)))
+                (if (not (nullable-null? pattern))
+                  pattern
+                  (truthStoreParseTimestampIso value))))))
+        (def (truthStoreJsonValueTimeLocal
+               (value : (Nullable Any))) : (Nullable Int)
+          (if (nullable-null? value)
+            (nullable-none Int)
+            (let ((raw (nullable-get value)))
+              (let ((number (truthStoreAnyAsNumber raw)))
+                (if (not (nullable-null? number))
+                  (nullable-some
+                    (truthStoreNumberToLong
+                      (nullable-get number)))
+                  (let ((text (truthStoreAnyAsString raw)))
+                    (if (nullable-null? text)
+                      (nullable-none Int)
+                      (truthStoreParseTimestampText
+                        (nullable-get text)))))))))
+        (def (truthStoreTruthTimeLocal
+               (truth : JSONObject)) : (Nullable Int)
+          (truthStoreJsonValueTimeLocal
+            (truthTimestampValue truth)))
+        (def (truthStoreJsonTimeLocal
+               (text : String)) : (Nullable Int)
+          (try
+            (truthStoreTruthTimeLocal
+              (jsonObjectFromText text))
+            (catch (error : Exception)
+              (nullable-none Int))))
+        (def (truthStoreShouldReplaceLocal
+               (local : File)
+               (remoteText : String)
+               (remoteModified : Int)) : Bool
+          (if (localFileMissing local)
+            #t
+            (let ((localText
+                    (truthStoreReadLocalText local)))
+              (if (localTextMatchesRemote localText remoteText)
+                #f
+                (shouldReplaceLocalByTimes
+                  (truthStoreJsonTimeLocal localText)
+                  (truthStoreJsonTimeLocal remoteText)
+                  remoteModified
+                  (truthStoreFileLastModified local))))))))
 
     (kotlin-file-lines "com/sfb/ssdreview/TruthStore.kt"
       (
@@ -16268,8 +16374,6 @@
        "import java.security.SecureRandom"
        "import java.security.cert.CertificateException"
        "import java.security.cert.X509Certificate"
-       "import java.time.OffsetDateTime"
-       "import java.time.format.DateTimeFormatter"
        "import java.util.zip.ZipEntry"
        "import java.util.zip.GZIPInputStream"
        "import java.util.zip.ZipInputStream"
@@ -16860,32 +16964,11 @@
        "        atomicWriteText(file, merged)"
        "    }"
        ""
-       "    internal fun shouldReplaceLocal(local: File, remoteText: String, remoteModified: Long): Boolean {"
-       "        if (localFileMissing(local)) return true"
-       "        val localText = readLocalText(local)"
-       "        if (localTextMatchesRemote(localText, remoteText)) return false"
-       "        val localGenerated = jsonTime(localText)"
-       "        val remoteGenerated = jsonTime(remoteText)"
-       "        return shouldReplaceLocalByTimes(localGenerated, remoteGenerated, remoteModified, local.lastModified())"
-       "    }"
+       "    internal fun shouldReplaceLocal(local: File, remoteText: String, remoteModified: Long): Boolean ="
+       "        truthStoreShouldReplaceLocal(local, remoteText, remoteModified)"
        ""
        "    private fun truthTime(truth: JSONObject): Long? ="
-       "        jsonValueTime(truthTimestampValue(truth))"
-       ""
-       "    private fun jsonTime(text: String): Long? = try {"
-       "        val json = jsonObjectFromText(text)"
-       "        truthTime(json)"
-       "    } catch (_: Exception) {"
-       "        null"
-       "    }"
-       ""
-       "    private fun jsonValueTime(value: Any?): Long? = when (value) {"
-       "        is Number -> value.toLong()"
-       "        is String -> value.toLongOrNull()"
-       "            ?: runCatching { OffsetDateTime.parse(value, DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ssZ\")).toInstant().toEpochMilli() }.getOrNull()"
-       "            ?: runCatching { OffsetDateTime.parse(value).toInstant().toEpochMilli() }.getOrNull()"
-       "        else -> null"
-       "    }"
+       "        truthStoreTruthTimeLocal(truth)"
        ""
        "    internal fun appendJsonLine(file: File, json: JSONObject) ="
        "        truthStoreAppendJsonLineLocal(this, file, json)"