Repository: splendo/kaluga
Issue: DateFormatter parsing Timezone inconsistently

**Describe the bug**
There is a platform difference with the timezone parsed by a DateFormatter.
On Android, the DateFormatter parses into the timezone of the date string, regardless of the timezone set by the formatter. Moreover, the formatter will change its timezone into the parsed timezone.

On iOS, the timezone is entirely ignored and the date is parsed into the default timezone.

This inconsistency should be resolved. Since the Kaluga Date object contains the TimeZone as an integral component, the proposed solution is to parse into the timezone set by the formatter (as opposed by the parsed string). This is partially because iOS does not seem to support parsing the timezone of the string, and partially because it makes the most sense when Date is in a timezone.

**To Reproduce**
```
val utcFormatter = DateFormatter.patternFormat("yyyy.MM.dd G 'at' HH:mm:ss z", TimeZone.utc, Locale.enUsPosix)
val pstFormatter = DateFormatter.patternFormat("yyyy.MM.dd G 'at' HH:mm:ss z", PSTTimeZone, Locale.enUsPosix)

val epochInUtc = Date.epoch(timeZone = TimeZone.utc, locale = Locale.enUsPosix)
val epochInPst = Date.epoch(timeZone = PSTTimeZone, locale = Locale.enUsPosix)
assertEquals("1970.01.01 AD at 00:00:00 ${TimeZone.utc.identifier}", utcFormatter.format(epochInUtc))
assertEquals("1970.01.01 AD at 00:00:00 ${TimeZone.utc.identifier}", utcFormatter.format(epochInPst))
assertEquals("1969.12.31 AD at 16:00:00 PST", pstFormatter.format(epochInUtc))
assertEquals("1969.12.31 AD at 16:00:00 PST", pstFormatter.format(epochInPst))
assertEquals(epochInUtc, utcFormatter.parse(utcFormatter.format(epochInUtc)))
assertEquals(epochInUtc, utcFormatter.parse(pstFormatter.format(epochInPst)))
assertEquals(epochInPst, pstFormatter.parse(utcFormatter.format(epochInUtc)))
assertEquals(epochInPst, pstFormatter.parse(pstFormatter.format(epochInPst)))
```
This code fails

**Expected behavior**
Parse into the timezone of the parser



Changed files:
base/src/iosMain/kotlin/text/DateFormatter.kt
base/src/androidLibMain/kotlin/text/DateFormatter.kt
base/src/commonTest/kotlin/text/DateFormatterTest.kt
base/src/jvmMain/kotlin/text/DateFormatter.kt

diff --git a/base/src/androidLibMain/kotlin/text/DateFormatter.kt b/base/src/androidLibMain/kotlin/text/DateFormatter.kt
index 6126e836d..d7f118c2e 100644
--- a/base/src/androidLibMain/kotlin/text/DateFormatter.kt
+++ b/base/src/androidLibMain/kotlin/text/DateFormatter.kt
@@ -124,13 +124,22 @@ actual class DateFormatter private constructor(private val format: SimpleDateFor
 
     actual fun format(date: Date): String = format.format(date.calendar.time)
     actual fun parse(string: String): Date? {
+        val currentTimeZone = timeZone
         return try {
             format.parse(string)?.let { date ->
                 val calendar = format.calendar.clone() as Calendar
-                Date(calendar.apply { time = date })
+                Date(
+                    calendar.apply {
+                        time = date
+                        timeZone = currentTimeZone.timeZone
+                    }
+                )
             }
         } catch (e: ParseException) {
             null
+        } finally {
+            // Parse may change the timezone to the timezone parsed by the String. This restores the original timezone
+            timeZone = currentTimeZone
         }
     }
 
diff --git a/base/src/commonTest/kotlin/text/DateFormatterTest.kt b/base/src/commonTest/kotlin/text/DateFormatterTest.kt
index 6881fbcc5..8aa801cca 100644
--- a/base/src/commonTest/kotlin/text/DateFormatterTest.kt
+++ b/base/src/commonTest/kotlin/text/DateFormatterTest.kt
@@ -22,8 +22,10 @@ import com.splendo.kaluga.base.text.DateFormatter
 import com.splendo.kaluga.base.text.dateFormat
 import com.splendo.kaluga.base.text.iso8601Pattern
 import com.splendo.kaluga.base.utils.Date
+import com.splendo.kaluga.base.utils.Locale
 import com.splendo.kaluga.base.utils.Locale.Companion.createLocale
 import com.splendo.kaluga.base.utils.TimeZone
+import com.splendo.kaluga.base.utils.enUsPosix
 import com.splendo.kaluga.base.utils.nowUtc
 import com.splendo.kaluga.base.utils.utc
 import kotlin.test.Test
@@ -64,6 +66,10 @@ class DateFormatterTest {
         assertEquals(2020, date.year)
         assertEquals(1, date.month)
         assertEquals(8, date.day)
+        assertEquals(0, date.hour)
+        assertEquals(0, date.minute)
+        assertEquals(0, date.second)
+        assertEquals(PSTTimeZone.identifier, date.timeZone.identifier)
     }
 
     @Test
@@ -104,4 +110,21 @@ class DateFormatterTest {
         val formatter = DateFormatter.iso8601Pattern(TimeZone.utc)
         assertNull(formatter.parse("invalid date"))
     }
+
+    @Test
+    fun testParseDateWithDifferentTimezone() {
+        val utcFormatter = DateFormatter.patternFormat("yyyy.MM.dd G 'at' HH:mm:ss z", TimeZone.utc, Locale.enUsPosix)
+        val pstFormatter = DateFormatter.patternFormat("yyyy.MM.dd G 'at' HH:mm:ss z", PSTTimeZone, Locale.enUsPosix)
+
+        val epochInUtc = Date.epoch(timeZone = TimeZone.utc, locale = Locale.enUsPosix)
+        val epochInPst = Date.epoch(timeZone = PSTTimeZone, locale = Locale.enUsPosix)
+        assertEquals("1970.01.01 AD at 00:00:00 ${TimeZone.utc.identifier}", utcFormatter.format(epochInUtc))
+        assertEquals("1970.01.01 AD at 00:00:00 ${TimeZone.utc.identifier}", utcFormatter.format(epochInPst))
+        assertEquals("1969.12.31 AD at 16:00:00 PST", pstFormatter.format(epochInUtc))
+        assertEquals("1969.12.31 AD at 16:00:00 PST", pstFormatter.format(epochInPst))
+        assertEquals(epochInUtc, utcFormatter.parse(utcFormatter.format(epochInUtc)))
+        assertEquals(epochInUtc, utcFormatter.parse(pstFormatter.format(epochInPst)))
+        assertEquals(epochInPst, pstFormatter.parse(utcFormatter.format(epochInUtc)))
+        assertEquals(epochInPst, pstFormatter.parse(pstFormatter.format(epochInPst)))
+    }
 }
diff --git a/base/src/iosMain/kotlin/text/DateFormatter.kt b/base/src/iosMain/kotlin/text/DateFormatter.kt
index 60d4c3442..2501cafe2 100644
--- a/base/src/iosMain/kotlin/text/DateFormatter.kt
+++ b/base/src/iosMain/kotlin/text/DateFormatter.kt
@@ -21,6 +21,7 @@ import com.splendo.kaluga.base.typedList
 import com.splendo.kaluga.base.utils.Date
 import com.splendo.kaluga.base.utils.Locale
 import com.splendo.kaluga.base.utils.TimeZone
+import com.splendo.kaluga.base.utils.utc
 import platform.Foundation.NSCalendar
 import platform.Foundation.NSDateFormatter
 import platform.Foundation.NSDateFormatterFullStyle
@@ -56,6 +57,16 @@ actual class DateFormatter private constructor(private val format: NSDateFormatt
             NSDateFormatter().apply {
                 this.locale = locale.nsLocale
                 this.timeZone = timeZone.timeZone
+                this.defaultDate = Date.now(timeZone = timeZone).apply {
+                    val epoch = Date.epoch(timeZone = TimeZone.utc)
+                    this.era = epoch.era
+                    this.year = epoch.year
+                    this.month = epoch.month
+                    this.day = epoch.day
+                    this.hour = epoch.hour
+                    this.minute = epoch.minute
+                    this.second = epoch.second
+                }.date
                 dateFormat = pattern
             }
         )
@@ -69,6 +80,16 @@ actual class DateFormatter private constructor(private val format: NSDateFormatt
             NSDateFormatter().apply {
                 this.locale = locale.nsLocale
                 this.timeZone = timeZone.timeZone
+                this.defaultDate = Date.now(timeZone = timeZone).apply {
+                    val epoch = Date.epoch(timeZone = TimeZone.utc)
+                    this.era = epoch.era
+                    this.year = epoch.year
+                    this.month = epoch.month
+                    this.day = epoch.day
+                    this.hour = epoch.hour
+                    this.minute = epoch.minute
+                    this.second = epoch.second
+                }.date
                 this.dateStyle = dateStyle.nsDateFormatterStyle()
                 this.timeStyle = timeStyle.nsDateFormatterStyle()
             }
@@ -112,6 +133,7 @@ actual class DateFormatter private constructor(private val format: NSDateFormatt
     actual fun parse(string: String): Date? {
         return format.dateFromString(string)?.let { date ->
             val calendar = format.calendar.copy() as NSCalendar
+            calendar.timeZone = timeZone.timeZone
             Date(calendar, date)
         }
     }
diff --git a/base/src/jvmMain/kotlin/text/DateFormatter.kt b/base/src/jvmMain/kotlin/text/DateFormatter.kt
index 4b3ac8b56..53d0e9835 100644
--- a/base/src/jvmMain/kotlin/text/DateFormatter.kt
+++ b/base/src/jvmMain/kotlin/text/DateFormatter.kt
@@ -127,13 +127,22 @@ actual class DateFormatter private constructor(private val format: SimpleDateFor
 
     actual fun format(date: Date): String = format.format(date.calendar.time)
     actual fun parse(string: String): Date? {
+        val currentTimeZone = timeZone
         return try {
             format.parse(string)?.let { date ->
                 val calendar = format.calendar.clone() as Calendar
-                Date(calendar.apply { time = date })
+                Date(
+                    calendar.apply {
+                        time = date
+                        timeZone = currentTimeZone.timeZone
+                    }
+                )
             }
         } catch (e: ParseException) {
             null
+        } finally {
+            // Parse may change the timezone to the timezone parsed by the String. This restores the original timezone
+            timeZone = currentTimeZone
         }
     }