diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt index deb8fdbc3..b7641cfbb 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt @@ -7,10 +7,9 @@ import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatterBuilder -import java.time.format.TextStyle import java.time.temporal.TemporalAccessor +import java.util.Calendar import java.util.Date -import java.util.Locale fun String.parseToZonedDateTime(): ZonedDateTime? { val parsed: TemporalAccessor = @@ -52,3 +51,18 @@ fun ZonedDateTime.matchesRepeatDays(repeatDays: Days?): Boolean { else -> false } } + +fun Calendar.matchesRepeatDays(repeatDays: Days?): Boolean { + repeatDays ?: return true // If no repeatDays specified, assume it matches + + return when (this.get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> repeatDays.m + Calendar.TUESDAY -> repeatDays.t + Calendar.WEDNESDAY -> repeatDays.w + Calendar.THURSDAY -> repeatDays.th + Calendar.FRIDAY -> repeatDays.f + Calendar.SATURDAY -> repeatDays.s + Calendar.SUNDAY -> repeatDays.su + else -> false + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Days.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Days.kt index 00d2de2ea..0282057ed 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Days.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Days.kt @@ -21,6 +21,23 @@ open class Days() : io.realm.RealmObject(), Parcelable { var s: Boolean = true var su: Boolean = true + constructor(m: Boolean? = null, + t: Boolean? = null, + w: Boolean? = null, + th: Boolean? = null, + f: Boolean? = null, + s: Boolean? = null, + su: Boolean? = null, + default: Boolean = true) : this() { + this.m = m ?: default + this.t = t ?: default + this.w = w ?: default + this.th = th ?: default + this.f = f ?: default + this.s = s ?: default + this.su = su ?: default + } + override fun writeToParcel( dest: Parcel, flags: Int, diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index abf21b2c3..b4f5ad8df 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -373,8 +373,7 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { if (remindersItem == null) return null val reminderTime = remindersItem.time?.parseToZonedDateTime() ?: return null - var dateTimeOccurenceToSchedule = - ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) + var dateTimeOccurenceToSchedule: ZonedDateTime val occurrencesList = mutableListOf() // If the reminder is a todo, only schedule sole dueDate/time occurrence. Otherwise, schedule multiple occurrences in advance @@ -388,28 +387,40 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { } val now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) var startDate = this.startDate?.toInstant()?.atZone(ZoneId.systemDefault()) ?: return null + val weekInMonth = getWeeksOfMonth()?.firstOrNull()?.toLong() + val weekdayInMonth = startDate.dayOfWeek val frequency = this.frequency ?: return null - val everyX = this.everyX ?: 1 - if (everyX == 0) { + val everyX = this.everyX?.toLong() ?: 1L + if (everyX == 0L) { return null } val repeatDays = this.repeat startDate = startDate.withHour(reminderTime.hour).withMinute(reminderTime.minute) + dateTimeOccurenceToSchedule = startDate + if (frequency == Frequency.MONTHLY) { + daysOfMonth?.let { + if (it.isEmpty()) return@let + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.withDayOfMonth(it.first()) + if (dateTimeOccurenceToSchedule.isBefore(startDate)) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusMonths(1) + } + } + } while (occurrencesList.size < occurrences) { // Increment currentDate based on the frequency dateTimeOccurenceToSchedule = when (frequency) { Frequency.DAILY -> { + while (dateTimeOccurenceToSchedule.isBefore(now)) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusDays(everyX) + } val todayWithTime = dateTimeOccurenceToSchedule .withHour(reminderTime.hour).withMinute(reminderTime.minute) - dateTimeOccurenceToSchedule = - if (dateTimeOccurenceToSchedule.isBefore(startDate)) { - startDate - } else if (occurrencesList.isEmpty() && todayWithTime.isAfter(now)) { + dateTimeOccurenceToSchedule = if (occurrencesList.isEmpty() && todayWithTime.isAfter(now)) { todayWithTime } else { - dateTimeOccurenceToSchedule.plusDays(everyX.toLong()) + dateTimeOccurenceToSchedule.plusDays(everyX) .withHour(reminderTime.hour).withMinute(reminderTime.minute) } dateTimeOccurenceToSchedule @@ -417,9 +428,18 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { Frequency.WEEKLY -> { // Set to start date if current date is earlier - if (dateTimeOccurenceToSchedule.isBefore(startDate)) { - dateTimeOccurenceToSchedule = startDate - } else if (repeatDays?.hasAnyDaySelected() == true) { + if (repeatDays?.hasAnyDaySelected() == true) { + if (dateTimeOccurenceToSchedule.isBefore(now)) { + dateTimeOccurenceToSchedule = + dateTimeOccurenceToSchedule.plusDays(1) + while (!dateTimeOccurenceToSchedule.matchesRepeatDays(repeatDays)) { + dateTimeOccurenceToSchedule = + dateTimeOccurenceToSchedule.plusDays(1) + } + } + while (dateTimeOccurenceToSchedule.isBefore(now)) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusDays(everyX * 7) + } var nextDueDate = dateTimeOccurenceToSchedule.withHour(reminderTime.hour) .withMinute(reminderTime.minute) @@ -458,35 +478,51 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { dateTimeOccurenceToSchedule = nextDueDate } // Set time to the reminder time - dateTimeOccurenceToSchedule = - dateTimeOccurenceToSchedule.withHour(reminderTime.hour) + dateTimeOccurenceToSchedule.withHour(reminderTime.hour) .withMinute(reminderTime.minute) - dateTimeOccurenceToSchedule } Frequency.MONTHLY -> { - dateTimeOccurenceToSchedule = - if (dateTimeOccurenceToSchedule.isBefore(startDate)) { - startDate - } else { - dateTimeOccurenceToSchedule.plusMonths(everyX.toLong()) - .withDayOfMonth(startDate.dayOfMonth).withHour(reminderTime.hour) - .withMinute(reminderTime.minute) + while (dateTimeOccurenceToSchedule.isBefore(now)) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusMonths(everyX) + if (weekInMonth != null && weekdayInMonth != null) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.withDayOfMonth(1) + while (dateTimeOccurenceToSchedule.dayOfWeek != weekdayInMonth) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusDays(1) + } + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusWeeks(weekInMonth) } - dateTimeOccurenceToSchedule + } + val todayWithTime = dateTimeOccurenceToSchedule + .withHour(reminderTime.hour).withMinute(reminderTime.minute) + if (occurrencesList.isEmpty() && todayWithTime.isAfter(now)) { + todayWithTime + } else if (weekInMonth != null && weekdayInMonth != null) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusMonths(everyX) + .withHour(reminderTime.hour).withMinute(reminderTime.minute) + .withDayOfMonth(1) + while (dateTimeOccurenceToSchedule.dayOfWeek != weekdayInMonth) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusDays(1) + } + dateTimeOccurenceToSchedule.plusWeeks(weekInMonth) + } else { + dateTimeOccurenceToSchedule.plusMonths(everyX) + .withHour(reminderTime.hour).withMinute(reminderTime.minute) + } } Frequency.YEARLY -> { - dateTimeOccurenceToSchedule = - if (dateTimeOccurenceToSchedule.isBefore(startDate)) { - startDate - } else { - dateTimeOccurenceToSchedule.plusYears(everyX.toLong()) - .withDayOfMonth(startDate.dayOfMonth) - .withMonth(startDate.monthValue).withHour(reminderTime.hour) - .withMinute(reminderTime.minute) - } - dateTimeOccurenceToSchedule + while (dateTimeOccurenceToSchedule.isBefore(now)) { + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusYears(everyX) + } + val todayWithTime = dateTimeOccurenceToSchedule + .withHour(reminderTime.hour).withMinute(reminderTime.minute) + if (occurrencesList.isEmpty() && todayWithTime.isAfter(now)) { + todayWithTime + } else { + dateTimeOccurenceToSchedule.plusYears(everyX) + .withHour(reminderTime.hour).withMinute(reminderTime.minute) + } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/SubscriptionPlan.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/SubscriptionPlan.kt index 39ffbec4e..e1d81b326 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/SubscriptionPlan.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/SubscriptionPlan.kt @@ -41,11 +41,13 @@ open class SubscriptionPlan : RealmObject(), BaseObject { val totalNumberOfGems: Int get() { + if (!isActive) return 0 return 24 + (consecutive?.gemCapExtra ?: 0) } val numberOfGemsLeft: Int get() { + if (!isActive) return 0 return totalNumberOfGems - (gemsBought ?: 0) } diff --git a/Habitica/src/test/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImplTest.kt b/Habitica/src/test/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImplTest.kt index abda1e5a7..2fa9444b9 100644 --- a/Habitica/src/test/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImplTest.kt +++ b/Habitica/src/test/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImplTest.kt @@ -39,13 +39,15 @@ class TaskRepositoryImplTest : WordSpec({ slot.captured(mockk(relaxed = true)) } val authenticationHandler = mockk() + every { authenticationHandler.currentUserID } answers { + "" + } repository = TaskRepositoryImpl( localRepository, apiClient, authenticationHandler, mockk(relaxed = true), - mockk(relaxed = true), ) val liveObjectSlot = slot() every { localRepository.getLiveObject(capture(liveObjectSlot)) } answers { diff --git a/Habitica/src/test/java/com/habitrpg/android/habitica/helpers/MarkdownProcessingTest.kt b/Habitica/src/test/java/com/habitrpg/android/habitica/helpers/MarkdownProcessingTest.kt index ffc350a93..f0eb6596a 100644 --- a/Habitica/src/test/java/com/habitrpg/android/habitica/helpers/MarkdownProcessingTest.kt +++ b/Habitica/src/test/java/com/habitrpg/android/habitica/helpers/MarkdownProcessingTest.kt @@ -1,9 +1,10 @@ package com.habitrpg.android.habitica.helpers +import com.habitrpg.common.habitica.helpers.MarkdownParser.preprocessImageMarkdown +import com.habitrpg.common.habitica.helpers.MarkdownParser.preprocessMarkdownLinks +import com.habitrpg.common.habitica.helpers.MarkdownParser.processMarkdown import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.shouldBe -import java.util.regex.Matcher -import java.util.regex.Pattern class MarkdownProcessingTest : WordSpec({ "processMarkdown" should { @@ -41,43 +42,4 @@ class MarkdownProcessingTest : WordSpec({ output shouldBe "![img](https://habitica-assets.s3.amazonaws.com/mobileApp/images/gold.png\"Habitica Gold\")" } } -}) { - companion object { - fun processMarkdown(input: String): String { - var processedInput = preprocessMarkdownLinks(input) - processedInput = preprocessImageMarkdown(processedInput) - return processedInput - } - - fun preprocessImageMarkdown(markdown: String): String { - val regex = Regex("""!\[.*?]\(.*?".*?"\)""") - return markdown.replace(regex) { matchResult -> - val match = matchResult.value - if (match.contains(".png\"")) { - match.replace(".png\"", ".png \"") - } else { - match - } - } - } - - fun preprocessMarkdownLinks(input: String): String { - val linkPattern = "\\[([^\\]]+)\\]\\(([^\\)]+)\\)" - val multilineLinkPattern = Pattern.compile(linkPattern, Pattern.DOTALL) - val matcher = multilineLinkPattern.matcher(input) - - val sb = StringBuffer(input.length) - - while (matcher.find()) { - val linkText = matcher.group(1) - val url = matcher.group(2) - val sanitizedUrl = url.replace(Regex("\\s"), "") - val correctedLink = "[$linkText]($sanitizedUrl)" - matcher.appendReplacement(sb, Matcher.quoteReplacement(correctedLink)) - } - matcher.appendTail(sb) - - return sb.toString() - } - } -} +}) \ No newline at end of file diff --git a/Habitica/src/test/java/com/habitrpg/android/habitica/models/tasks/TaskTest.kt b/Habitica/src/test/java/com/habitrpg/android/habitica/models/tasks/TaskTest.kt new file mode 100644 index 000000000..89285e212 --- /dev/null +++ b/Habitica/src/test/java/com/habitrpg/android/habitica/models/tasks/TaskTest.kt @@ -0,0 +1,295 @@ +package com.habitrpg.android.habitica.models.tasks + +import com.habitrpg.android.habitica.extensions.matchesRepeatDays +import com.habitrpg.android.habitica.extensions.toZonedDateTime +import com.habitrpg.shared.habitica.models.tasks.Frequency +import com.habitrpg.shared.habitica.models.tasks.TaskType +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.shouldBe +import java.time.format.DateTimeFormatter +import java.util.Calendar + +class TaskTest : WordSpec({ + "getNextReminderOccurrences" When { + var daily = Task() + var reminder = RemindersItem() + var calendar = Calendar.getInstance() + beforeEach { + daily = Task() + daily.type = TaskType.DAILY + + reminder = RemindersItem() + + calendar = Calendar.getInstance() + } + "dailies repeating daily" should { + beforeEach { + daily.frequency = Frequency.DAILY + } + "return occurrences according to everyX = 2" { + calendar.add(Calendar.DATE, -2) + daily.startDate = calendar.time + daily.everyX = 2 + calendar.add(Calendar.DATE, 4) + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + occurrences?.forEach { + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + calendar.add(Calendar.DATE, 2) + } + } + + "return occurrences according to everyX = 1" { + calendar.add(Calendar.DATE, -2) + daily.startDate = calendar.time + daily.everyX = 1 + calendar.add(Calendar.DATE, 3) + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + occurrences?.forEach { + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + calendar.add(Calendar.DATE, 1) + } + } + + "return occurrences according to everyX = 5" { + calendar.add(Calendar.DATE, -33) + daily.startDate = calendar.time + daily.everyX = 5 + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + calendar.add(Calendar.DATE, 35) + occurrences?.forEach { + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + calendar.add(Calendar.DATE, 5) + } + } + } + "dailies repeating weekly" should { + beforeEach { + daily.frequency = Frequency.WEEKLY + } + "return occurrences if active every day" { + daily.startDate = calendar.time + daily.repeat = Days() + daily.everyX = 1 + + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + calendar.add(Calendar.DATE, 1) + occurrences?.forEach { + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + calendar.add(Calendar.DATE, 1) + } + } + + "return occurrences if active tuesdays" { + calendar.add(Calendar.DATE, -63) + daily.startDate = calendar.time + daily.repeat = Days(t = true, default = false) + daily.everyX = 1 + + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + calendar.add(Calendar.DATE, 63) + while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.TUESDAY) { + calendar.add(Calendar.DATE, 1) + } + occurrences?.forEach { + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + calendar.add(Calendar.DATE, 7) + } + } + + "return occurrences if active every second friday" { + calendar.add(Calendar.DATE, -23) + daily.startDate = calendar.time + daily.repeat = Days(f = true, default = false) + daily.everyX = 2 + + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.FRIDAY) { + calendar.add(Calendar.DATE, 1) + } + calendar.add(Calendar.DATE, 28) + occurrences?.forEach { + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + calendar.add(Calendar.DATE, 14) + } + } + + "return occurrences if active multiple days a week" { + calendar.add(Calendar.DATE, -23) + daily.startDate = calendar.time + daily.repeat = Days(t = true, f = true, s = true, default = false) + daily.everyX = 1 + + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + calendar.add(Calendar.DATE, 23) + occurrences?.forEach { + while (!calendar.matchesRepeatDays(daily.repeat)) { + calendar.add(Calendar.DATE, 1) + } + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + calendar.add(Calendar.DATE, 1) + } + } + } + + "dailies repeating monthly" should { + beforeEach { + daily.frequency = Frequency.MONTHLY + } + + "return occurrences if active every 10th of the month" { + daily.startDate = calendar.time + daily.everyX = 1 + daily.setDaysOfMonth(listOf(10)) + + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + occurrences?.forEach { + while (calendar.get(Calendar.DAY_OF_MONTH) != 10) { + calendar.add(Calendar.DATE, 1) + } + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + it.year shouldBe calendar.get(Calendar.YEAR) + calendar.add(Calendar.DATE, 1) + } + } + + "return occurrences if active every third month on the 5th" { + calendar.add(Calendar.MONDAY, -8) + daily.startDate = calendar.time + daily.everyX = 3 + daily.setDaysOfMonth(listOf(5)) + + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + calendar.add(Calendar.MONTH, 9) + occurrences?.size shouldBe 4 + occurrences?.forEach { + while (calendar.get(Calendar.DAY_OF_MONTH) != 5) { + calendar.add(Calendar.DATE, 1) + } + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + it.year shouldBe calendar.get(Calendar.YEAR) + calendar.add(Calendar.MONTH, 3) + } + } + + "return occurrences if active every month on the third tuesday" { + calendar.set(Calendar.YEAR, 2025) + calendar.set(Calendar.MONTH, Calendar.JANUARY) + calendar.set(Calendar.DAY_OF_MONTH, 21) + daily.startDate = calendar.time + daily.everyX = 1 + daily.setWeeksOfMonth(listOf(2)) + + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + calendar.set(Calendar.DAY_OF_MONTH, 1) + occurrences?.forEach { + while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.TUESDAY) { + calendar.add(Calendar.DATE, 1) + } + calendar.add(Calendar.DATE, 14) + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + it.year shouldBe calendar.get(Calendar.YEAR) + calendar.add(Calendar.MONTH, 1) + calendar.set(Calendar.DAY_OF_MONTH, 1) + } + } + + "return occurrences if active every fifth month on the second wednesday" { + calendar.add(Calendar.MONTH, -8) + calendar.set(Calendar.DAY_OF_MONTH, 1) + while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.WEDNESDAY) { + calendar.add(Calendar.DATE, 1) + } + calendar.add(Calendar.DATE, 7) + daily.startDate = calendar.time + daily.everyX = 5 + daily.setWeeksOfMonth(listOf(1)) + + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + calendar.add(Calendar.MONTH, 10) + calendar.set(Calendar.DAY_OF_MONTH, 1) + occurrences?.forEach { + while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.WEDNESDAY) { + calendar.add(Calendar.DATE, 1) + } + calendar.add(Calendar.DATE, 7) + print(daily.startDate) + print(it) + println(calendar.time) + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + it.year shouldBe calendar.get(Calendar.YEAR) + calendar.add(Calendar.MONTH, 5) + calendar.set(Calendar.DAY_OF_MONTH, 1) + } + } + } + + "dailies repeating yearly" should { + beforeEach { + daily.frequency = Frequency.YEARLY + } + "return occurrences if active every year" { + daily.startDate = calendar.time + daily.everyX = 1 + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + calendar.add(Calendar.YEAR, 1) + occurrences?.forEach { + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + it.year shouldBe calendar.get(Calendar.YEAR) + calendar.add(Calendar.YEAR, 1) + } + } + + "return occurrences if active every 3 years" { + calendar.add(Calendar.YEAR, -3) + calendar.add(Calendar.DATE, 1) + daily.startDate = calendar.time + daily.everyX = 3 + reminder.time = calendar.time.toZonedDateTime()?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + calendar.add(Calendar.YEAR, 3) + val occurrences = daily.getNextReminderOccurrences(reminder, 4) + occurrences?.size shouldBe 4 + occurrences?.forEach { + it.dayOfYear shouldBe calendar.get(Calendar.DAY_OF_YEAR) + it.year shouldBe calendar.get(Calendar.YEAR) + calendar.add(Calendar.YEAR, 3) + } + } + } + } +}) diff --git a/Habitica/src/test/java/com/habitrpg/android/habitica/models/user/SubscriptionPlanTest.kt b/Habitica/src/test/java/com/habitrpg/android/habitica/models/user/SubscriptionPlanTest.kt index 19c0be234..b2dde0ec5 100644 --- a/Habitica/src/test/java/com/habitrpg/android/habitica/models/user/SubscriptionPlanTest.kt +++ b/Habitica/src/test/java/com/habitrpg/android/habitica/models/user/SubscriptionPlanTest.kt @@ -33,13 +33,13 @@ class SubscriptionPlanTest : WordSpec({ plan.totalNumberOfGems shouldBe 0 } - "25 without extra consecutive bonus" { - plan.totalNumberOfGems shouldBe 25 + "24 without extra consecutive bonus" { + plan.totalNumberOfGems shouldBe 24 } - "35 with extra consecutive bonus" { + "40 with extra consecutive bonus" { plan.consecutive = SubscriptionPlanConsecutive() - plan.consecutive?.gemCapExtra = 15 + plan.consecutive?.gemCapExtra = 16 plan.totalNumberOfGems shouldBe 40 } } @@ -52,57 +52,14 @@ class SubscriptionPlanTest : WordSpec({ "according to already purchased amount" { plan.gemsBought = 10 - plan.numberOfGemsLeft shouldBe 15 + plan.numberOfGemsLeft shouldBe 14 } "according to already purchased amount with bonus" { plan.consecutive = SubscriptionPlanConsecutive() plan.consecutive?.gemCapExtra = 10 plan.gemsBought = 10 - plan.numberOfGemsLeft shouldBe 25 - } - } - - "monthsUntilNextHourglass" should { - beforeEach { - plan.consecutive = SubscriptionPlanConsecutive() - plan.consecutive?.count = 0 - plan.dateTerminated = null - } - - "months until next hourglass with initial basic sub" { - plan.planId = SubscriptionPlan.PLANID_BASIC - plan.monthsUntilNextHourglass shouldBe 3 - } - - "months until receiving first hourglass with basic sub" { - plan.consecutive?.count = 2 - plan.planId = SubscriptionPlan.PLANID_BASIC - plan.monthsUntilNextHourglass shouldBe 1 - } - - "months until next hourglass with basic sub after receiving initial hourglass" { - plan.consecutive?.count = 3 - plan.planId = SubscriptionPlan.PLANID_BASIC - plan.monthsUntilNextHourglass shouldBe 3 - } - - "months until next hourglass with three month sub" { - plan.consecutive?.offset = 3 - plan.planId = SubscriptionPlan.PLANID_BASIC3MONTH - plan.monthsUntilNextHourglass shouldBe 3 - } - - "months until next hourglass with six month sub" { - plan.consecutive?.offset = 6 - plan.planId = SubscriptionPlan.PLANID_BASIC6MONTH - plan.monthsUntilNextHourglass shouldBe 6 - } - - "months until next hourglass with 12 month sub" { - plan.consecutive?.offset = 12 - plan.planId = SubscriptionPlan.PLANID_BASIC12MONTH - plan.monthsUntilNextHourglass shouldBe 12 + plan.numberOfGemsLeft shouldBe 24 } } }) diff --git a/Habitica/src/test/java/com/habitrpg/android/habitica/utils/SerializerSpec.kt b/Habitica/src/test/java/com/habitrpg/android/habitica/utils/SerializerSpec.kt index a6c7cd780..1450e9e71 100644 --- a/Habitica/src/test/java/com/habitrpg/android/habitica/utils/SerializerSpec.kt +++ b/Habitica/src/test/java/com/habitrpg/android/habitica/utils/SerializerSpec.kt @@ -6,7 +6,7 @@ import io.kotest.core.spec.DslDrivenSpec import io.kotest.core.spec.style.scopes.WordSpecRootScope import io.mockk.mockk -open class SerializerSpec(body: SerializerSpec.() -> Unit = {}) : +abstract class SerializerSpec(body: SerializerSpec.() -> Unit = {}) : DslDrivenSpec(), WordSpecRootScope { val deserializationContext: JsonDeserializationContext = mockk(relaxed = true) diff --git a/common/src/main/java/com/habitrpg/common/habitica/helpers/MarkdownParser.kt b/common/src/main/java/com/habitrpg/common/habitica/helpers/MarkdownParser.kt index 4d8b97158..fecc87dcb 100644 --- a/common/src/main/java/com/habitrpg/common/habitica/helpers/MarkdownParser.kt +++ b/common/src/main/java/com/habitrpg/common/habitica/helpers/MarkdownParser.kt @@ -122,14 +122,14 @@ object MarkdownParser { return result } - private fun processMarkdown(input: String): String { + fun processMarkdown(input: String): String { var processedInput = preprocessMarkdownLinks(input) processedInput = preprocessImageMarkdown(processedInput) processedInput = preprocessHtmlTags(processedInput) return processedInput } - private fun preprocessImageMarkdown(markdown: String): String { + fun preprocessImageMarkdown(markdown: String): String { // Used to handle an image tag with a URL that ends with .jpg or .png (Else the image may be shown as broken, a link, or not at all) // Example: (..ample_image_name.png"Zombie hatching potion") -> (..ample_image_name.png "Zombie hatching potion") val regex = Regex("""!\[.*?]\(.*?".*?"\)""") @@ -145,8 +145,8 @@ object MarkdownParser { } } - private fun preprocessMarkdownLinks(input: String): String { - val linkPattern = "\\[([^\\]]+)\\]\\(([^\\)]+)\\)" + fun preprocessMarkdownLinks(input: String): String { + val linkPattern = "\\[([^\\]]+)\\]\\(([^\\)\"]+)(\".*\")?\\)" val multilineLinkPattern = Pattern.compile(linkPattern, Pattern.DOTALL) val matcher = multilineLinkPattern.matcher(input) @@ -155,8 +155,9 @@ object MarkdownParser { while (matcher.find()) { val linkText = matcher.group(1) val url = matcher.group(2) + val description = matcher.group(3) val sanitizedUrl = url?.replace(Regex("\\s"), "") - val correctedLink = "[$linkText]($sanitizedUrl)" + val correctedLink = if (description.isNullOrBlank()) "[$linkText]($sanitizedUrl)" else "[$linkText]($sanitizedUrl$description)" matcher.appendReplacement(sb, Matcher.quoteReplacement(correctedLink)) } matcher.appendTail(sb)