fix various issues with task reminder scheduling

This commit is contained in:
Phillip Thelen 2025-01-09 17:51:05 +01:00
parent d4e4b1bcb5
commit 087c0b06ed
10 changed files with 419 additions and 133 deletions

View file

@ -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
}
}

View file

@ -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,

View file

@ -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<ZonedDateTime>()
// 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)
}
}
}

View file

@ -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)
}

View file

@ -39,13 +39,15 @@ class TaskRepositoryImplTest : WordSpec({
slot.captured(mockk(relaxed = true))
}
val authenticationHandler = mockk<AuthenticationHandler>()
every { authenticationHandler.currentUserID } answers {
""
}
repository =
TaskRepositoryImpl(
localRepository,
apiClient,
authenticationHandler,
mockk(relaxed = true),
mockk(relaxed = true),
)
val liveObjectSlot = slot<BaseObject>()
every { localRepository.getLiveObject(capture(liveObjectSlot)) } answers {

View file

@ -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()
}
}
}
})

View file

@ -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)
}
}
}
}
})

View file

@ -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
}
}
})

View file

@ -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)

View file

@ -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)