mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 19:56:32 +00:00
fix various issues with task reminder scheduling
This commit is contained in:
parent
d4e4b1bcb5
commit
087c0b06ed
10 changed files with 419 additions and 133 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
}) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue