diff --git a/.gitignore b/.gitignore index 1e3603076..a8c758cef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,16 @@ *.iml +*.pbxuser +*.perspective +*.perspectivev3 +*.swp +*~.nib +.DS_Store +.externalNativeBuild .gradle -local.properties .idea -.DS_Store build +build/ captures -.externalNativeBuild +local.properties node_modules +*xcuserdata* diff --git a/core/build.gradle b/core/build.gradle index 8f9db33da..fc0db1931 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -26,7 +26,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:3.2.1" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.11" } } @@ -42,17 +42,11 @@ apply plugin:"kotlin-multiplatform" kotlin { targets { + def iosPreset = System.getenv('SDK_NAME')?.startsWith("iphoneos") ? presets.iosArm64 : presets.iosX64 fromPreset(presets.jvm, 'jvm') - - fromPreset(presets.iosX64, 'iOS') { + fromPreset(iosPreset, 'iOS') { compilations.main.outputKinds('FRAMEWORK') } - - // Replace the target above by the following one to produce a framework - // which can be installed on a real iPhone: - // fromPreset(presets.iosArm64, 'iOS') { - // compilations.main.outputKinds('FRAMEWORK') - // } } sourceSets { diff --git a/core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt b/core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt index d05fd2fb0..37ee103f3 100644 --- a/core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt +++ b/core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt @@ -31,48 +31,143 @@ enum class DayOfWeek(val index: Int) { SATURDAY(6), } -data class Timestamp(val unixTimeInMillis: Long) +data class Timestamp(val millisSince1970: Long) { + val localDate: LocalDate + get() { + val millisSince2000 = millisSince1970 - 946684800000 + val daysSince2000 = millisSince2000 / 86400000 + return LocalDate(daysSince2000.toInt()) + } +} + +data class LocalDate(val daysSince2000: Int) { + + var yearCache = -1 + var monthCache = -1 + var dayCache = -1 + + init { + if (daysSince2000 < 0) + throw IllegalArgumentException("$daysSince2000 < 0") + } + + constructor(year: Int, month: Int, day: Int) : + this(daysSince2000(year, month, day)) + + val dayOfWeek: DayOfWeek + get() { + return when (daysSince2000 % 7) { + 0 -> DayOfWeek.SATURDAY + 1 -> DayOfWeek.SUNDAY + 2 -> DayOfWeek.MONDAY + 3 -> DayOfWeek.TUESDAY + 4 -> DayOfWeek.WEDNESDAY + 5 -> DayOfWeek.THURSDAY + else -> DayOfWeek.FRIDAY + } + } + + val timestamp: Timestamp + get() { + return Timestamp(946684800000 + daysSince2000.toLong() * 86400000) + } + + val year: Int + get() { + if (yearCache < 0) updateYearMonthDayCache() + return yearCache + } + + val month: Int + get() { + if (monthCache < 0) updateYearMonthDayCache() + return monthCache + } + + val day: Int + get() { + if (dayCache < 0) updateYearMonthDayCache() + return dayCache + } + + private fun updateYearMonthDayCache() { + var currYear = 2000 + var currDay = 0 + + while (true) { + val currYearLength = if (isLeapYear(currYear)) 366 else 365 + if (daysSince2000 < currDay + currYearLength) { + yearCache = currYear + break + } else { + currYear++ + currDay += currYearLength + } + } + + var currMonth = 1 + val monthOffset = if (isLeapYear(currYear)) leapOffset else nonLeapOffset + + while (true) { + if (daysSince2000 < currDay + monthOffset[currMonth]) { + monthCache = currMonth + break + } else { + currMonth++ + } + } -data class LocalDate(val year: Int, - val month: Int, - val day: Int) { + currDay += monthOffset[currMonth - 1] + dayCache = daysSince2000 - currDay + 1 + + } fun isOlderThan(other: LocalDate): Boolean { - if (other.year != year) return other.year > year - if (other.month != month) return other.month > month - return other.day > day + return daysSince2000 < other.daysSince2000 } fun isNewerThan(other: LocalDate): Boolean { - if (this == other) return false - return other.isOlderThan(this) + return daysSince2000 > other.daysSince2000 } - init { - if ((month <= 0) or (month >= 13)) throw(IllegalArgumentException()) - if ((day <= 0) or (day >= 32)) throw(IllegalArgumentException()) + fun plus(days: Int): LocalDate { + return LocalDate(daysSince2000 + days) } -} -interface LocalDateCalculator { - fun plusDays(date: LocalDate, days: Int): LocalDate - fun dayOfWeek(date: LocalDate): DayOfWeek - fun toTimestamp(date: LocalDate): Timestamp - fun fromTimestamp(timestamp: Timestamp): LocalDate -} - -fun LocalDateCalculator.distanceInDays(d1: LocalDate, d2: LocalDate): Int { - val t1 = toTimestamp(d1) - val t2 = toTimestamp(d2) - val dayLength = 24 * 60 * 60 * 1000 - return abs((t2.unixTimeInMillis - t1.unixTimeInMillis) / dayLength).toInt() -} + fun minus(days: Int): LocalDate { + return LocalDate(daysSince2000 - days) + } -fun LocalDateCalculator.minusDays(date: LocalDate, days: Int): LocalDate { - return plusDays(date, -days) + fun distanceTo(other: LocalDate): Int { + return abs(daysSince2000 - other.daysSince2000) + } } interface LocalDateFormatter { fun shortWeekdayName(date: LocalDate): String fun shortMonthName(date: LocalDate): String +} + +private fun isLeapYear(year: Int): Boolean { + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} + +val leapOffset = arrayOf(0, 31, 60, 91, 121, 152, 182, + 213, 244, 274, 305, 335, 366) +val nonLeapOffset = arrayOf(0, 31, 59, 90, 120, 151, 181, + 212, 243, 273, 304, 334, 365) + +private fun daysSince2000(year: Int, month: Int, day: Int): Int { + + var result = 365 * (year - 2000) + result += ceil((year - 2000) / 4.0).toInt() + result -= ceil((year - 2000) / 100.0).toInt() + result += ceil((year - 2000) / 400.0).toInt() + if (isLeapYear(year)) { + result += leapOffset[month - 1] + } else { + result += nonLeapOffset[month - 1] + } + result += (day - 1) + return result } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt index 12310d3b9..53f2f3e56 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt @@ -30,7 +30,6 @@ class Backend(databaseName: String, databaseOpener: DatabaseOpener, fileOpener: FileOpener, val log: Log, - val dateCalculator: LocalDateCalculator, val taskRunner: TaskRunner) { val database: Database @@ -56,13 +55,12 @@ class Backend(databaseName: String, database = databaseOpener.open(dbFile) database.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log) habitsRepository = HabitRepository(database) - checkmarkRepository = CheckmarkRepository(database, dateCalculator) + checkmarkRepository = CheckmarkRepository(database) taskRunner.runInBackground { habits.putAll(habitsRepository.findAll()) for ((key, habit) in habits) { val checks = checkmarkRepository.findAll(key) - checkmarks[habit] = CheckmarkList(habit.frequency, - dateCalculator) + checkmarks[habit] = CheckmarkList(habit.frequency) checkmarks[habit]?.setManualCheckmarks(checks) } } @@ -76,7 +74,7 @@ class Backend(databaseName: String, habit.id = id habit.position = habits.size habits[id] = habit - checkmarks[habit] = CheckmarkList(habit.frequency, dateCalculator) + checkmarks[habit] = CheckmarkList(habit.frequency) habitsRepository.insert(habit) mainScreenDataSource.requestData() } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/components/CalendarChart.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/components/CalendarChart.kt index 09c149830..02ba821c1 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/components/CalendarChart.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/components/CalendarChart.kt @@ -26,7 +26,6 @@ import kotlin.math.* class CalendarChart(var today: LocalDate, var color: Color, var theme: Theme, - var dateCalculator: LocalDateCalculator, var dateFormatter: LocalDateFormatter) : Component { var padding = 5.0 @@ -47,19 +46,19 @@ class CalendarChart(var today: LocalDate, canvas.setFontSize(height * 0.06) val nColumns = floor((width - 2 * padding) / squareSize).toInt() - 2 - val todayWeekday = dateCalculator.dayOfWeek(today) + val todayWeekday = today.dayOfWeek val topLeftOffset = (nColumns - 1 + scrollPosition) * 7 + todayWeekday.index - val topLeftDate = dateCalculator.minusDays(today, topLeftOffset) + val topLeftDate = today.minus(topLeftOffset) repeat(nColumns) { column -> val topOffset = topLeftOffset - 7 * column - val topDate = dateCalculator.plusDays(topLeftDate, 7 * column) + val topDate = topLeftDate.plus(7 * column) drawColumn(canvas, column, topDate, topOffset) } canvas.setColor(theme.mediumContrastTextColor) repeat(7) { row -> - val date = dateCalculator.plusDays(topLeftDate, row) + val date = topLeftDate.plus(row) canvas.setTextAlign(TextAlign.LEFT) canvas.drawText(dateFormatter.shortWeekdayName(date), padding + nColumns * squareSize + padding, @@ -74,7 +73,7 @@ class CalendarChart(var today: LocalDate, drawHeader(canvas, column, topDate) repeat(7) { row -> val offset = topOffset - row - val date = dateCalculator.plusDays(topDate, row) + val date = topDate.plus(row) if (offset < 0) return drawSquare(canvas, padding + column * squareSize, diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/components/HabitListHeader.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/components/HabitListHeader.kt index 6d4ec6f28..f3e401fa4 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/components/HabitListHeader.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/components/HabitListHeader.kt @@ -25,8 +25,7 @@ import org.isoron.platform.time.* class HabitListHeader(private val today: LocalDate, private val nButtons: Int, private val theme: Theme, - private val fmt: LocalDateFormatter, - private val calc: LocalDateCalculator) : Component { + private val fmt: LocalDateFormatter) : Component { override fun draw(canvas: Canvas) { val width = canvas.getWidth() @@ -44,7 +43,7 @@ class HabitListHeader(private val today: LocalDate, canvas.setFontSize(theme.smallTextSize) repeat(nButtons) { index -> - val date = calc.minusDays(today, nButtons - index - 1) + val date = today.minus(nButtons - index - 1) val name = fmt.shortWeekdayName(date).toUpperCase() val number = date.day.toString() diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt index 6856a3ae3..9a9be2b8d 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt @@ -24,8 +24,7 @@ import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_AUTOMATIC import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_MANUAL import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED -class CheckmarkList(private val frequency: Frequency, - private val dateCalculator: LocalDateCalculator) { +class CheckmarkList(private val frequency: Frequency) { private val manualCheckmarks = mutableListOf() private val automaticCheckmarks = mutableListOf() @@ -39,8 +38,7 @@ class CheckmarkList(private val frequency: Frequency, automaticCheckmarks.clear() manualCheckmarks.addAll(checks) automaticCheckmarks.addAll(computeAutomaticCheckmarks(checks, - frequency, - dateCalculator)) + frequency)) } /** @@ -56,7 +54,7 @@ class CheckmarkList(private val frequency: Frequency, val result = mutableListOf() val newest = automaticCheckmarks.first().date - val distToNewest = dateCalculator.distanceInDays(newest, date) + val distToNewest = newest.distanceTo(date) var fromIndex = 0 val toIndex = automaticCheckmarks.size @@ -75,13 +73,12 @@ class CheckmarkList(private val frequency: Frequency, * Computes the list of automatic checkmarks a list of manual ones. */ fun computeAutomaticCheckmarks(checks: List, - frequency: Frequency, - calc: LocalDateCalculator + frequency: Frequency ): MutableList { - val intervals = buildIntervals(checks, frequency, calc) - snapIntervalsTogether(intervals, calc) - return buildCheckmarksFromIntervals(checks, intervals, calc) + val intervals = buildIntervals(checks, frequency) + snapIntervalsTogether(intervals) + return buildCheckmarksFromIntervals(checks, intervals) } /** @@ -92,19 +89,17 @@ class CheckmarkList(private val frequency: Frequency, * the interval, however, still falls within the interval. The length of * the intervals are also not modified. */ - fun snapIntervalsTogether(intervals: MutableList, - calc: LocalDateCalculator) { + fun snapIntervalsTogether(intervals: MutableList) { for (i in 1 until intervals.size) { val (begin, center, end) = intervals[i] val (_, _, prevEnd) = intervals[i - 1] - val gap = calc.distanceInDays(prevEnd, begin) - 1 - val endMinusGap = calc.minusDays(end, gap) - if (gap <= 0 || endMinusGap.isOlderThan(center)) continue - intervals[i] = Interval(calc.minusDays(begin, gap), + val gap = prevEnd.distanceTo(begin) - 1 + if (gap <= 0 || end.minus(gap).isOlderThan(center)) continue + intervals[i] = Interval(begin.minus(gap), center, - calc.minusDays(end, gap)) + end.minus(gap)) } } @@ -119,8 +114,7 @@ class CheckmarkList(private val frequency: Frequency, * receive unchecked checkmarks. */ fun buildCheckmarksFromIntervals(checks: List, - intervals: List, - calc: LocalDateCalculator + intervals: List ): MutableList { if (checks.isEmpty()) throw IllegalArgumentException() @@ -137,25 +131,25 @@ class CheckmarkList(private val frequency: Frequency, if (check.date.isNewerThan(newest)) newest = check.date } - val distance = calc.distanceInDays(oldest, newest) + val distance = oldest.distanceTo(newest) val checkmarks = mutableListOf() for (offset in 0..distance) - checkmarks.add(Checkmark(calc.minusDays(newest, offset), + checkmarks.add(Checkmark(newest.minus(offset), UNCHECKED)) for (interval in intervals) { - val beginOffset = calc.distanceInDays(newest, interval.begin) - val endOffset = calc.distanceInDays(newest, interval.end) + val beginOffset = newest.distanceTo(interval.begin) + val endOffset = newest.distanceTo(interval.end) for (offset in endOffset..beginOffset) { checkmarks.set(offset, - Checkmark(calc.minusDays(newest, offset), + Checkmark(newest.minus(offset), CHECKED_AUTOMATIC)) } } for (check in checks) { - val offset = calc.distanceInDays(newest, check.date) + val offset = newest.distanceTo(check.date) checkmarks.set(offset, Checkmark(check.date, CHECKED_MANUAL)) } @@ -167,8 +161,7 @@ class CheckmarkList(private val frequency: Frequency, * checkmarks. */ fun buildIntervals(checks: List, - frequency: Frequency, - calc: LocalDateCalculator): MutableList { + frequency: Frequency): MutableList { val num = frequency.numerator val den = frequency.denominator @@ -178,10 +171,9 @@ class CheckmarkList(private val frequency: Frequency, val first = checks[i] val last = checks[i + num - 1] - val distance = calc.distanceInDays(first.date, last.date) - if (distance >= den) continue + if (first.date.distanceTo(last.date) >= den) continue - val end = calc.plusDays(first.date, den - 1) + val end = first.date.plus(den - 1) intervals.add(Interval(first.date, last.date, end)) } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkRepository.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkRepository.kt index 169ea4f0c..0b35bc275 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkRepository.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkRepository.kt @@ -22,8 +22,7 @@ package org.isoron.uhabits.models import org.isoron.platform.io.* import org.isoron.platform.time.* -class CheckmarkRepository(db: Database, - val dateCalculator: LocalDateCalculator) { +class CheckmarkRepository(db: Database) { private val findStatement = db.prepareStatement("select timestamp, value from Repetitions where habit = ? order by timestamp desc") private val insertStatement = db.prepareStatement("insert into Repetitions(habit, timestamp, value) values (?, ?, ?)") @@ -33,9 +32,8 @@ class CheckmarkRepository(db: Database, findStatement.bindInt(0, habitId) val result = mutableListOf() while (findStatement.step() == StepResult.ROW) { - val timestamp = Timestamp(findStatement.getLong(0)) + val date = Timestamp(findStatement.getLong(0)).localDate val value = findStatement.getInt(1) - val date = dateCalculator.fromTimestamp(timestamp) result.add(Checkmark(date, value)) } findStatement.reset() @@ -43,18 +41,18 @@ class CheckmarkRepository(db: Database, } fun insert(habitId: Int, checkmark: Checkmark) { - val timestamp = dateCalculator.toTimestamp(checkmark.date) + val timestamp = checkmark.date.timestamp insertStatement.bindInt(0, habitId) - insertStatement.bindLong(1, timestamp.unixTimeInMillis) + insertStatement.bindLong(1, timestamp.millisSince1970) insertStatement.bindInt(2, checkmark.value) insertStatement.step() insertStatement.reset() } fun delete(habitId: Int, date: LocalDate) { - val timestamp = dateCalculator.toTimestamp(date) + val timestamp = date.timestamp deleteStatement.bindInt(0, habitId) - deleteStatement.bindLong(1, timestamp.unixTimeInMillis) + deleteStatement.bindLong(1, timestamp.millisSince1970) deleteStatement.step() deleteStatement.reset() } diff --git a/core/src/jvmMain/kotlin/org/isoron/platform/time/JavaDates.kt b/core/src/jvmMain/kotlin/org/isoron/platform/time/JavaDates.kt index ae8c8e958..72a385aa3 100644 --- a/core/src/jvmMain/kotlin/org/isoron/platform/time/JavaDates.kt +++ b/core/src/jvmMain/kotlin/org/isoron/platform/time/JavaDates.kt @@ -19,19 +19,20 @@ package org.isoron.platform.time -import java.lang.Math.* import java.util.* import java.util.Calendar.* + fun LocalDate.toGregorianCalendar(): GregorianCalendar { - val cal = GregorianCalendar(TimeZone.getTimeZone("GMT")) - cal.set(Calendar.HOUR_OF_DAY, 0) - cal.set(Calendar.MINUTE, 0) - cal.set(Calendar.SECOND, 0) - cal.set(Calendar.MILLISECOND, 0) - cal.set(Calendar.YEAR, this.year) - cal.set(Calendar.MONTH, this.month - 1) - cal.set(Calendar.DAY_OF_MONTH, this.day) + val cal = GregorianCalendar() + cal.timeZone = TimeZone.getTimeZone("GMT") + cal.set(MILLISECOND, 0) + cal.set(SECOND, 0) + cal.set(MINUTE, 0) + cal.set(HOUR_OF_DAY, 0) + cal.set(YEAR, this.year) + cal.set(MONTH, this.month - 1) + cal.set(DAY_OF_MONTH, this.day) return cal } @@ -56,35 +57,3 @@ class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter { return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale); } } - -class JavaLocalDateCalculator : LocalDateCalculator { - override fun toTimestamp(date: LocalDate): Timestamp { - val cal = date.toGregorianCalendar() - return Timestamp(cal.timeInMillis) - } - - override fun fromTimestamp(timestamp: Timestamp): LocalDate { - val cal = GregorianCalendar(TimeZone.getTimeZone("GMT")) - cal.timeInMillis = timestamp.unixTimeInMillis - return cal.toLocalDate() - } - - override fun dayOfWeek(date: LocalDate): DayOfWeek { - val cal = date.toGregorianCalendar() - return when (cal.get(DAY_OF_WEEK)) { - Calendar.SATURDAY -> DayOfWeek.SATURDAY - Calendar.SUNDAY -> DayOfWeek.SUNDAY - Calendar.MONDAY -> DayOfWeek.MONDAY - Calendar.TUESDAY -> DayOfWeek.TUESDAY - Calendar.WEDNESDAY -> DayOfWeek.WEDNESDAY - Calendar.THURSDAY -> DayOfWeek.THURSDAY - else -> DayOfWeek.FRIDAY - } - } - - override fun plusDays(date: LocalDate, days: Int): LocalDate { - val cal = date.toGregorianCalendar() - cal.add(Calendar.DAY_OF_MONTH, days) - return cal.toLocalDate() - } -} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatesTest.kt b/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatesTest.kt index d88dfe577..e74d7b64c 100644 --- a/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatesTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/platform/JavaDatesTest.kt @@ -23,10 +23,10 @@ import junit.framework.TestCase.* import org.isoron.platform.time.* import org.junit.* import java.util.* +import java.util.Calendar.* class JavaDatesTest { - private val calc = JavaLocalDateCalculator() private val d1 = LocalDate(2019, 3, 25) private val d2 = LocalDate(2019, 4, 4) private val d3 = LocalDate(2019, 5, 12) @@ -34,46 +34,46 @@ class JavaDatesTest { @Test fun plusMinusDays() { val today = LocalDate(2019, 3, 25) - assertEquals(calc.minusDays(today, 28), LocalDate(2019, 2, 25)) - assertEquals(calc.plusDays(today, 7), LocalDate(2019, 4, 1)) - assertEquals(calc.plusDays(today, 42), LocalDate(2019, 5, 6)) + assertEquals(today.minus(28), LocalDate(2019, 2, 25)) + assertEquals(today.plus(7), LocalDate(2019, 4, 1)) + assertEquals(today.plus(42), LocalDate(2019, 5, 6)) } @Test fun shortMonthName() { var fmt = JavaLocalDateFormatter(Locale.US) - assertEquals(fmt.shortWeekdayName(d1), "Mon") - assertEquals(fmt.shortWeekdayName(d2), "Thu") - assertEquals(fmt.shortWeekdayName(d3), "Sun") - assertEquals(fmt.shortMonthName(d1), "Mar") - assertEquals(fmt.shortMonthName(d2), "Apr") - assertEquals(fmt.shortMonthName(d3), "May") + assertEquals("Mon", fmt.shortWeekdayName(d1)) + assertEquals("Thu", fmt.shortWeekdayName(d2)) + assertEquals("Sun", fmt.shortWeekdayName(d3)) + assertEquals("Mar", fmt.shortMonthName(d1)) + assertEquals("Apr", fmt.shortMonthName(d2)) + assertEquals("May", fmt.shortMonthName(d3)) fmt = JavaLocalDateFormatter(Locale.JAPAN) - assertEquals(fmt.shortWeekdayName(d1), "月") - assertEquals(fmt.shortWeekdayName(d2), "木") - assertEquals(fmt.shortWeekdayName(d3), "日") - assertEquals(fmt.shortMonthName(d1), "3月") - assertEquals(fmt.shortMonthName(d2), "4月") - assertEquals(fmt.shortMonthName(d3), "5月") + assertEquals("月", fmt.shortWeekdayName(d1)) + assertEquals("木", fmt.shortWeekdayName(d2)) + assertEquals("日", fmt.shortWeekdayName(d3)) + assertEquals("3月", fmt.shortMonthName(d1)) + assertEquals("4月", fmt.shortMonthName(d2)) + assertEquals("5月", fmt.shortMonthName(d3)) } @Test fun weekDay() { - assertEquals(DayOfWeek.SUNDAY, calc.dayOfWeek(LocalDate(2015, 1, 25))) - assertEquals(DayOfWeek.MONDAY, calc.dayOfWeek(LocalDate(2017, 7, 3))) + assertEquals(DayOfWeek.SUNDAY, LocalDate(2015, 1, 25).dayOfWeek) + assertEquals(DayOfWeek.MONDAY, LocalDate(2017, 7, 3).dayOfWeek) } @Test fun timestamps() { val timestamps = listOf(Timestamp(1555977600000), Timestamp(968716800000), - Timestamp(0)) + Timestamp(946684800000)) val dates = listOf(LocalDate(2019, 4, 23), LocalDate(2000, 9, 12), - LocalDate(1970, 1, 1)) - assertEquals(timestamps, dates.map { d -> calc.toTimestamp(d) }) - assertEquals(dates, timestamps.map { t -> calc.fromTimestamp(t) }) + LocalDate(2000, 1, 1)) + assertEquals(timestamps, dates.map { d -> d.timestamp }) + assertEquals(dates, timestamps.map { t -> t.localDate }) } @Test @@ -96,10 +96,47 @@ class JavaDatesTest { val d2 = LocalDate(2019, 5, 30) val d3 = LocalDate(2019, 6, 5) - assertEquals(0, calc.distanceInDays(d1, d1)) - assertEquals(20, calc.distanceInDays(d1, d2)) - assertEquals(20, calc.distanceInDays(d2, d1)) - assertEquals(26, calc.distanceInDays(d1, d3)) - assertEquals(6, calc.distanceInDays(d2, d3)) + assertEquals(0, d1.distanceTo(d1)) + assertEquals(20, d1.distanceTo(d2)) + assertEquals(20, d2.distanceTo(d1)) + assertEquals(26, d1.distanceTo(d3)) + assertEquals(6, d2.distanceTo(d3)) + } + + @Test + fun gregorianCalendarConversion() { + fun check(cal: GregorianCalendar, daysSince2000: Int) { + val year = cal.get(YEAR) + val month = cal.get(MONTH) + 1 + val day = cal.get(DAY_OF_MONTH) + val weekday = cal.get(DAY_OF_WEEK) + val date = LocalDate(year, month, day) + val millisSince1970 = cal.timeInMillis + val msg = "date=$year-$month-$day offset=$daysSince2000" + + assertEquals(msg, daysSince2000, date.daysSince2000) + assertEquals(msg, year, date.year) + assertEquals(msg, month, date.month) + assertEquals(msg, day, date.day) + assertEquals(msg, weekday, date.dayOfWeek.index + 1) + assertEquals(msg, millisSince1970, date.timestamp.millisSince1970) + assertEquals(msg, date, date.timestamp.localDate) + } + + val cal = GregorianCalendar() + cal.timeZone = TimeZone.getTimeZone("GMT") + cal.set(MILLISECOND, 0) + cal.set(SECOND, 0) + cal.set(MINUTE, 0) + cal.set(HOUR_OF_DAY, 0) + cal.set(DAY_OF_MONTH, 1) + cal.set(MONTH, 0) + cal.set(YEAR, 2000) + + // Check all dates from year 2000 until 2400 + for(offset in 0..146097) { + check(cal, offset) + cal.add(DAY_OF_YEAR, 1) + } } } diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/Base.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/Base.kt index e4a99dd18..75cbc83d7 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/Base.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/Base.kt @@ -38,8 +38,6 @@ open class BaseTest { val databaseOpener = JavaDatabaseOpener(log) - val dateCalculator = JavaLocalDateCalculator() - val taskRunner = SequentialTaskRunner() lateinit var db: Database diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/backend/BackendTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/backend/BackendTest.kt index 5c845e194..3b9a4ff07 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/backend/BackendTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/backend/BackendTest.kt @@ -40,7 +40,6 @@ class BackendTest : BaseTest() { databaseOpener, fileOpener, log, - dateCalculator, taskRunner) } diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CalendarChartTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CalendarChartTest.kt index 3474d0658..dae5670aa 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CalendarChartTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/CalendarChartTest.kt @@ -32,7 +32,6 @@ class CalendarChartTest : BaseViewTest() { val component = CalendarChart(LocalDate(2015, 1, 25), theme.color(4), theme, - JavaLocalDateCalculator(), JavaLocalDateFormatter(Locale.US)) component.series = listOf(1.0, // today 0.2, 0.5, 0.7, 0.0, 0.3, 0.4, 0.6, diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/components/HabitListHeaderTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/HabitListHeaderTest.kt index 6086575cc..3173328ca 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/components/HabitListHeaderTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/components/HabitListHeaderTest.kt @@ -30,8 +30,7 @@ class HabitListHeaderTest : BaseViewTest() { val header = HabitListHeader(LocalDate(2019, 3, 25), 5, theme, - JavaLocalDateFormatter(Locale.US), - JavaLocalDateCalculator()) + JavaLocalDateFormatter(Locale.US)) assertRenders(1200, 96, "components/HabitListHeader/light.png", header) diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt index b85650784..6ecab8319 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt @@ -32,7 +32,7 @@ class CheckmarkListTest : BaseTest() { private val today = LocalDate(2019, 1, 30) private fun day(offset: Int): LocalDate { - return dateCalculator.minusDays(today, offset) + return today.minus(offset) } @Test @@ -45,8 +45,7 @@ class CheckmarkListTest : BaseTest() { CheckmarkList.Interval(day(18), day(18), day(12)), CheckmarkList.Interval(day(8), day(8), day(2))) val actual = CheckmarkList.buildIntervals(checks, - Frequency.WEEKLY, - dateCalculator) + Frequency.WEEKLY) assertEquals(expected, actual) } @@ -60,8 +59,7 @@ class CheckmarkListTest : BaseTest() { CheckmarkList.Interval(day(18), day(18), day(18)), CheckmarkList.Interval(day(8), day(8), day(8))) val actual = CheckmarkList.buildIntervals(checks, - Frequency.DAILY, - dateCalculator) + Frequency.DAILY) assertEquals(expected, actual) } @@ -77,8 +75,7 @@ class CheckmarkListTest : BaseTest() { CheckmarkList.Interval(day(22), day(18), day(16)), CheckmarkList.Interval(day(18), day(15), day(12))) val actual = CheckmarkList.buildIntervals(checks, - Frequency.TWO_TIMES_PER_WEEK, - dateCalculator) + Frequency.TWO_TIMES_PER_WEEK) assertEquals(expected, actual) } @@ -94,7 +91,7 @@ class CheckmarkListTest : BaseTest() { CheckmarkList.Interval(day(25), day(25), day(19)), CheckmarkList.Interval(day(18), day(16), day(12)), CheckmarkList.Interval(day(11), day(8), day(5))) - CheckmarkList.snapIntervalsTogether(original, dateCalculator) + CheckmarkList.snapIntervalsTogether(original) assertEquals(expected, original) } @@ -118,8 +115,7 @@ class CheckmarkListTest : BaseTest() { Checkmark(day(9), CHECKED_AUTOMATIC), Checkmark(day(10), CHECKED_MANUAL)) val actual = CheckmarkList.buildCheckmarksFromIntervals(checks, - intervals, - dateCalculator) + intervals) assertEquals(expected, actual) } @@ -134,8 +130,7 @@ class CheckmarkListTest : BaseTest() { Checkmark(day(4), CHECKED_AUTOMATIC), Checkmark(day(5), CHECKED_AUTOMATIC)) val actual = CheckmarkList.buildCheckmarksFromIntervals(reps, - intervals, - dateCalculator) + intervals) assertEquals(expected, actual) } @@ -158,14 +153,13 @@ class CheckmarkListTest : BaseTest() { Checkmark(day(9), CHECKED_AUTOMATIC), Checkmark(day(10), CHECKED_MANUAL)) val actual = CheckmarkList.computeAutomaticCheckmarks(checks, - Frequency(1, 3), - dateCalculator) + Frequency(1, 3)) assertEquals(expected, actual) } @Test fun testGetValuesUntil() { - val list = CheckmarkList(Frequency(1, 2), dateCalculator) + val list = CheckmarkList(Frequency(1, 2)) list.setManualCheckmarks(listOf(Checkmark(day(4), CHECKED_MANUAL), Checkmark(day(7), CHECKED_MANUAL))) val expected = listOf(UNCHECKED, @@ -188,7 +182,7 @@ class CheckmarkListTest : BaseTest() { @Test fun testGetValuesUntil2() { - val list = CheckmarkList(Frequency(1, 2), dateCalculator) + val list = CheckmarkList(Frequency(1, 2)) val expected = listOf() assertEquals(expected, list.getValuesUntil(day(0))) } diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt index 8f8414621..55c09bf3f 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkRepositoryTest.kt @@ -39,7 +39,7 @@ class CheckmarkRepositoryTest : BaseTest() { Checkmark(LocalDate(2019, 1, 25), 450), Checkmark(LocalDate(2019, 1, 20), 1000)) - val repository = CheckmarkRepository(db, JavaLocalDateCalculator()) + val repository = CheckmarkRepository(db) for (c in checkmarksA) repository.insert(habitA, c) for (c in checkmarksB) repository.insert(habitB, c) diff --git a/ios/Application/AppDelegate.swift b/ios/Application/AppDelegate.swift index 5656064d4..436df40ba 100644 --- a/ios/Application/AppDelegate.swift +++ b/ios/Application/AppDelegate.swift @@ -26,7 +26,6 @@ import UIKit databaseOpener: IosDatabaseOpener(withLog: StandardLog()), fileOpener: IosFileOpener(), log: StandardLog(), - dateCalculator: IosLocalDateCalculator(), taskRunner: SequentialTaskRunner()) func application(_ application: UIApplication, @@ -35,7 +34,7 @@ import UIKit window = UIWindow(frame: UIScreen.main.bounds) if let window = window { let nav = UINavigationController() - nav.viewControllers = [ListHabitsController(withBackend: backend)] + nav.viewControllers = [MainScreenController(withBackend: backend)] window.backgroundColor = UIColor.white window.rootViewController = nav window.makeKeyAndVisible() diff --git a/ios/Application/Frontend/ListHabitsController.swift b/ios/Application/Frontend/MainScreenController.swift similarity index 94% rename from ios/Application/Frontend/ListHabitsController.swift rename to ios/Application/Frontend/MainScreenController.swift index 103885541..b5d1a36d7 100644 --- a/ios/Application/Frontend/ListHabitsController.swift +++ b/ios/Application/Frontend/MainScreenController.swift @@ -19,7 +19,7 @@ import UIKit -class ListHabitsCell : UITableViewCell { +class MainScreenCell : UITableViewCell { var ring: ComponentView var label = UILabel() var buttons: [ComponentView] = [] @@ -48,7 +48,7 @@ class ListHabitsCell : UITableViewCell { label.heightAnchor.constraint(equalToConstant: size).isActive = true stack.addArrangedSubview(label) - for _ in 1...4 { + for _ in 1...3 { let btn = ComponentView(frame: frame, component: nil) btn.backgroundColor = .white btn.widthAnchor.constraint(equalToConstant: size).isActive = true @@ -88,7 +88,7 @@ class ListHabitsCell : UITableViewCell { } } -class ListHabitsController: UITableViewController, MainScreenDataSourceListener { +class MainScreenController: UITableViewController, MainScreenDataSourceListener { var backend: Backend var dataSource: MainScreenDataSource var data: MainScreenDataSource.Data? @@ -119,7 +119,7 @@ class ListHabitsController: UITableViewController, MainScreenDataSourceListener target: self, action: #selector(self.onCreateHabitClicked)) ] - tableView.register(ListHabitsCell.self, forCellReuseIdentifier: "cell") + tableView.register(MainScreenCell.self, forCellReuseIdentifier: "cell") tableView.backgroundColor = theme.headerBackgroundColor.uicolor } @@ -140,7 +140,7 @@ class ListHabitsController: UITableViewController, MainScreenDataSourceListener override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let row = indexPath.row - let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ListHabitsCell + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MainScreenCell let color = theme.color(paletteIndex: data!.colors[row].index) cell.label.text = data!.names[row] cell.setColor(color) @@ -149,10 +149,9 @@ class ListHabitsController: UITableViewController, MainScreenDataSourceListener override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let component = HabitListHeader(today: LocalDate(year: 2019, month: 3, day: 24), - nButtons: 4, + nButtons: 3, theme: theme, - fmt: IosLocalDateFormatter(), - calc: IosLocalDateCalculator()) + fmt: IosLocalDateFormatter()) return ComponentView(frame: CGRect(x: 0, y: 0, width: 100, height: CGFloat(theme.checkmarkButtonSize)), component: component) } diff --git a/ios/Application/Frontend/ShowHabitController.swift b/ios/Application/Frontend/ShowHabitController.swift index 352f7170a..d85a7687a 100644 --- a/ios/Application/Frontend/ShowHabitController.swift +++ b/ios/Application/Frontend/ShowHabitController.swift @@ -47,7 +47,6 @@ class ShowHabitController : UITableViewController { let component = CalendarChart(today: LocalDate(year: 2019, month: 3, day: 15), color: color, theme: theme, - dateCalculator: IosLocalDateCalculator(), dateFormatter: IosLocalDateFormatter()) let cell = UITableViewCell() let view = ComponentView(frame: cell.frame, component: component) diff --git a/ios/Application/Platform/IosDates.swift b/ios/Application/Platform/IosDates.swift index 9c2da3be5..457f0878e 100644 --- a/ios/Application/Platform/IosDates.swift +++ b/ios/Application/Platform/IosDates.swift @@ -54,35 +54,3 @@ class IosLocalDateFormatter : NSObject, LocalDateFormatter { return fmt.string(from: date.iosDate) } } - -class IosLocalDateCalculator : NSObject, LocalDateCalculator { - func toTimestamp(date: LocalDate) -> Timestamp { - return Timestamp(unixTimeInMillis: Int64(date.iosDate.timeIntervalSince1970 * 1000)) - } - - func fromTimestamp(timestamp: Timestamp) -> LocalDate { - return Date.init(timeIntervalSince1970: Double(timestamp.unixTimeInMillis / 1000)).localDate - } - - let calendar = Calendar(identifier: .gregorian) - - func dayOfWeek(date: LocalDate) -> DayOfWeek { - let weekday = calendar.component(.weekday, from: date.iosDate) - switch(weekday) { - case 1: return DayOfWeek.sunday - case 2: return DayOfWeek.monday - case 3: return DayOfWeek.tuesday - case 4: return DayOfWeek.wednesday - case 5: return DayOfWeek.thursday - case 6: return DayOfWeek.friday - default: return DayOfWeek.saturday - } - } - - func plusDays(date: LocalDate, days: Int32) -> LocalDate { - let d2 = date.iosDate.addingTimeInterval(24.0 * 60 * 60 * Double(days)) - return LocalDate(year: Int32(calendar.component(.year, from: d2)), - month: Int32(calendar.component(.month, from: d2)), - day: Int32(calendar.component(.day, from: d2))) - } -} diff --git a/ios/uhabits.xcodeproj/project.pbxproj b/ios/uhabits.xcodeproj/project.pbxproj index d8e74fd21..85ffd573c 100644 --- a/ios/uhabits.xcodeproj/project.pbxproj +++ b/ios/uhabits.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 0057EC2B224C4CDB00C49288 /* icons in Resources */ = {isa = PBXBuildFile; fileRef = 0057EC2A224C4CDB00C49288 /* icons */; }; 00A5B42822009F590024E00C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A5B42722009F590024E00C /* AppDelegate.swift */; }; - 00A5B42A22009F590024E00C /* ListHabitsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A5B42922009F590024E00C /* ListHabitsController.swift */; }; + 00A5B42A22009F590024E00C /* MainScreenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A5B42922009F590024E00C /* MainScreenController.swift */; }; 00A5B42F22009F5A0024E00C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00A5B42E22009F5A0024E00C /* Assets.xcassets */; }; 00C0C6A52246537A003D8AF0 /* IosFilesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6A122465365003D8AF0 /* IosFilesTest.swift */; }; 00C0C6A62246537E003D8AF0 /* IosDatabaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6A222465365003D8AF0 /* IosDatabaseTest.swift */; }; @@ -59,7 +59,7 @@ 0057EC2A224C4CDB00C49288 /* icons */ = {isa = PBXFileReference; lastKnownFileType = folder; path = icons; sourceTree = ""; }; 00A5B42422009F590024E00C /* uhabits.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = uhabits.app; sourceTree = BUILT_PRODUCTS_DIR; }; 00A5B42722009F590024E00C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 00A5B42922009F590024E00C /* ListHabitsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListHabitsController.swift; sourceTree = ""; }; + 00A5B42922009F590024E00C /* MainScreenController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenController.swift; sourceTree = ""; }; 00A5B42E22009F5A0024E00C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 00A5B43322009F5A0024E00C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00A5B43822009F5A0024E00C /* uhabitsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = uhabitsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -107,7 +107,7 @@ isa = PBXGroup; children = ( 00D48BD22200AC1600CC4527 /* EditHabitController.swift */, - 00A5B42922009F590024E00C /* ListHabitsController.swift */, + 00A5B42922009F590024E00C /* MainScreenController.swift */, 00C0C6DE224A35FC003D8AF0 /* ShowHabitController.swift */, ); path = Frontend; @@ -317,7 +317,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd ../core\n./gradlew linkDebugFrameworkIOS\n"; + shellScript = "cd ../core\n./gradlew linkIOS\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -332,7 +332,7 @@ 00C0C6E0224A3602003D8AF0 /* ShowHabitController.swift in Sources */, 00C0C6A8224654A2003D8AF0 /* IosDatabase.swift in Sources */, 00C0C6DB2247E6B0003D8AF0 /* IosDates.swift in Sources */, - 00A5B42A22009F590024E00C /* ListHabitsController.swift in Sources */, + 00A5B42A22009F590024E00C /* MainScreenController.swift in Sources */, 00A5B42822009F590024E00C /* AppDelegate.swift in Sources */, 00D48BD32200AC1600CC4527 /* EditHabitController.swift in Sources */, ); diff --git a/ios/uhabits.xcodeproj/project.xcworkspace/xcuserdata/isoron.xcuserdatad/UserInterfaceState.xcuserstate b/ios/uhabits.xcodeproj/project.xcworkspace/xcuserdata/isoron.xcuserdatad/UserInterfaceState.xcuserstate index 174c153cf..767e37baf 100644 Binary files a/ios/uhabits.xcodeproj/project.xcworkspace/xcuserdata/isoron.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/uhabits.xcodeproj/project.xcworkspace/xcuserdata/isoron.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/uhabits.xcodeproj/xcuserdata/isoron.xcuserdatad/xcschemes/uhabits.xcscheme b/ios/uhabits.xcodeproj/xcuserdata/isoron.xcuserdatad/xcschemes/uhabits.xcscheme index 8ed318948..b4eae5b1a 100644 --- a/ios/uhabits.xcodeproj/xcuserdata/isoron.xcuserdatad/xcschemes/uhabits.xcscheme +++ b/ios/uhabits.xcodeproj/xcuserdata/isoron.xcuserdatad/xcschemes/uhabits.xcscheme @@ -83,7 +83,8 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> - + - +