Skip to content

Commit

Permalink
fix(timezones): wrong hours ahead calculation (and daylight saving ti…
Browse files Browse the repository at this point in the history
…mes) (closes #341)
  • Loading branch information
Bnyro committed Feb 7, 2025
1 parent fa104f9 commit 23b9cfe
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 20 deletions.
156 changes: 156 additions & 0 deletions app/schemas/com.bnyro.clock.data.database.AppDatabase/10.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "6f143cf30194ea20e9bb17f4388738a1",
"entities": [
{
"tableName": "timeZones",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `zoneId` TEXT NOT NULL, `zoneName` TEXT NOT NULL, `countryName` TEXT NOT NULL, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "zoneId",
"columnName": "zoneId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "zoneName",
"columnName": "zoneName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "countryName",
"columnName": "countryName",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "alarms",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `time` INTEGER NOT NULL, `label` TEXT, `enabled` INTEGER NOT NULL, `days` TEXT NOT NULL, `vibrate` INTEGER NOT NULL, `soundName` TEXT, `soundUri` TEXT, `repeat` INTEGER NOT NULL DEFAULT 1, `snoozeEnabled` INTEGER NOT NULL DEFAULT 1, `snoozeMinutes` INTEGER NOT NULL DEFAULT 10, `soundEnabled` INTEGER NOT NULL DEFAULT 1, `vibrationPattern` TEXT NOT NULL DEFAULT '1000,1000,1000,1000,1000', `vibrationPatternName` TEXT NOT NULL DEFAULT 'Default')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "days",
"columnName": "days",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "vibrate",
"columnName": "vibrate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "soundName",
"columnName": "soundName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "soundUri",
"columnName": "soundUri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "repeat",
"columnName": "repeat",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "snoozeEnabled",
"columnName": "snoozeEnabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "snoozeMinutes",
"columnName": "snoozeMinutes",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "10"
},
{
"fieldPath": "soundEnabled",
"columnName": "soundEnabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "vibrationPattern",
"columnName": "vibrationPattern",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'1000,1000,1000,1000,1000'"
},
{
"fieldPath": "vibrationPatternName",
"columnName": "vibrationPatternName",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'Default'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6f143cf30194ea20e9bb17f4388738a1')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import com.bnyro.clock.domain.model.TimeZone

@Database(
entities = [TimeZone::class, Alarm::class],
version = 9,
version = 10,
autoMigrations = [
AutoMigration(
from = 2,
Expand All @@ -28,14 +28,18 @@ import com.bnyro.clock.domain.model.TimeZone
AutoMigration(from = 4, to = 5),
AutoMigration(from = 5, to = 6),
AutoMigration(from = 6, to = 7),
AutoMigration(from = 8, to = 9)
AutoMigration(from = 8, to = 9),
AutoMigration(from = 9, to = 10, spec = AppDatabase.RemoveTimeZoneOffsetColumn::class)
]
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@DeleteColumn("alarms", "sound")
class RemoveSoundColumnAutoMigration : AutoMigrationSpec

@DeleteColumn(tableName = "timeZones", columnName = "offset")
class RemoveTimeZoneOffsetColumn : AutoMigrationSpec

abstract fun timeZonesDao(): TimeZonesDao
abstract fun alarmsDao(): AlarmsDao

Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/com/bnyro/clock/domain/model/TimeZone.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ data class TimeZone(
@PrimaryKey
val key: String,
val zoneId: String,
val offset: Int,
val zoneName: String,
val countryName: String = ""
val countryName: String = "",
) {
override fun equals(other: Any?): Boolean {
if (other is TimeZone) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@ class TimezoneRepository(private val timeZonesDao: TimeZonesDao) {

private fun getTimezonesForCountries(zoneIds: List<CountryTimezone>): List<TimeZone> {
return zoneIds.map {
val zone = java.util.TimeZone.getTimeZone(it.zoneId)
val zoneKey = arrayOf(it.zoneId, it.zoneName, it.countryName).joinToString(",")
val offset = zone.getOffset(Calendar.getInstance().timeInMillis)
TimeZone(zoneKey, it.zoneId, offset, it.zoneName, it.countryName)
TimeZone(zoneKey, it.zoneId, it.zoneName, it.countryName)
}.sortedBy { it.zoneName }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ private fun TimeZoneCard(
modifier = Modifier.padding(
horizontal = 16.dp, vertical = 8.dp
),
text = TimeHelper.formatGMTTimeDifference(timeZone.offset.toFloat() / 1000 / 3600),
text = TimeHelper.formatGMTTimeDifference(timeZone.zoneId),
color = MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.titleMedium
)
Expand All @@ -282,7 +282,6 @@ private fun TimeZoneCardPreview() {
key = "America/New_York,New_York,United States",
zoneName = "New_York",
countryName = "United States",
offset = -14400000,
zoneId = "America/New_York"
), selected = true, onClick = {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ClockModel(application: Application) : AndroidViewModel(application) {
val zones = selectedZones.distinct()
when (sortOrder) {
SortOrder.ALPHABETIC -> zones.sortedBy { it.zoneName }
SortOrder.OFFSET -> zones.sortedBy { it.offset }
SortOrder.OFFSET -> zones.sortedBy { TimeHelper.getOffsetMillisByZoneId(it.zoneId) }
}
}.stateIn(
scope = viewModelScope,
Expand Down
33 changes: 23 additions & 10 deletions app/src/main/java/com/bnyro/clock/util/TimeHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Calendar
import java.util.Date
import java.util.GregorianCalendar
import java.util.TimeZone
import kotlin.math.abs
import kotlin.time.Duration
Expand Down Expand Up @@ -53,19 +54,27 @@ object TimeHelper {
return timeFormatter.format(time)
}

fun getOffsetMillisByZoneId(timeZoneId: String): Int {
val zone = TimeZone.getTimeZone(timeZoneId)
return zone.getOffset(Calendar.getInstance().timeInMillis)
}

@SuppressLint("DefaultLocale")
fun formatGMTTimeDifference(timeDiff: Float): String {
val prefix = if (timeDiff >= 0) "+" else "-"
val hours = abs(timeDiff.toInt())
val minutes = (timeDiff * 60f % 60).toInt()
fun formatGMTTimeDifference(timeZoneId: String): String {
val offset = getOffsetMillisByZoneId(timeZoneId)

return if (minutes == 0) {
"GMT $prefix$hours"
} else {
val formattedMinutes = String.format("%02d", minutes)
"GMT $prefix$hours:$formattedMinutes"
val hours = offset / 1000 / 60 / 60
val minutes = (offset / 1000 / 60) % 60

var offsetString = hours.toString()
if (hours > 0) {
offsetString = "+$offsetString"
}
if (minutes != 0) {
offsetString += ":$minutes"
}

return offsetString
}

/**
Expand Down Expand Up @@ -102,7 +111,11 @@ object TimeHelper {
context: Context,
timeZone: com.bnyro.clock.domain.model.TimeZone
): String {
val millisOffset = (timeZone.offset - TimeZone.getDefault().rawOffset)

This comment has been minimized.

Copy link
@soshial

soshial Feb 10, 2025

GregorianCalendar is bad. Please, use Instant.now()

val now = GregorianCalendar.getInstance().timeInMillis
val currentZoneOffset = TimeZone.getDefault().getOffset(now)
val otherZoneOffset = getOffsetMillisByZoneId(timeZone.zoneId)

val millisOffset = otherZoneOffset - currentZoneOffset
val minutesOffset = millisOffset / MILLIS_PER_MINUTE
val hours = minutesOffset.div(MINUTES_PER_HOUR)
val minutes = abs(minutesOffset.mod(MINUTES_PER_HOUR))
Expand Down

0 comments on commit 23b9cfe

Please sign in to comment.