Coverage Summary for Class: EventsScheduleParser (com.galarzaa.tibiakt.core.section.news.event.parser)

Class Class, % Method, % Branch, % Line, % Instruction, %
EventsScheduleParser 100% (1/1) 100% (2/2) 83.3% (20/24) 97.7% (42/43) 94% (281/299)


 /*
  * Copyright © 2025 Allan Galarza
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 package com.galarzaa.tibiakt.core.section.news.event.parser
 
 import com.galarzaa.tibiakt.core.exceptions.ParsingException
 import com.galarzaa.tibiakt.core.html.cells
 import com.galarzaa.tibiakt.core.html.cleanText
 import com.galarzaa.tibiakt.core.html.parsePopup
 import com.galarzaa.tibiakt.core.parser.Parser
 import com.galarzaa.tibiakt.core.section.news.event.builder.EventsScheduleBuilder
 import com.galarzaa.tibiakt.core.section.news.event.builder.eventEntryBuilder
 import com.galarzaa.tibiakt.core.section.news.event.builder.eventsSchedule
 import com.galarzaa.tibiakt.core.section.news.event.model.EventsSchedule
 import com.galarzaa.tibiakt.core.text.remove
 import com.galarzaa.tibiakt.core.time.FORMAT_YEAR_MONTH
 import kotlinx.datetime.DatePeriod
 import kotlinx.datetime.LocalDate
 import kotlinx.datetime.YearMonth
 import kotlinx.datetime.minus
 import kotlinx.datetime.minusMonth
 import kotlinx.datetime.plusMonth
 import org.jsoup.nodes.Element
 
 /** Parser for the events schedule. */
 public object EventsScheduleParser : Parser<EventsSchedule> {
     override fun fromContent(content: String): EventsSchedule {
         val boxContent = boxContent(content)
         return eventsSchedule {
             val dateBlock = boxContent.selectFirst("div.eventscheduleheaderdateblock")
                 ?: throw ParsingException("date block not found")
             yearMonth =
                 YearMonth.parse(dateBlock.cleanText().remove("»").remove("«"), FORMAT_YEAR_MONTH)
             parseCalendar(boxContent, yearMonth)
         }
 
     }
 
     private fun EventsScheduleBuilder.parseCalendar(
         boxContent: Element,
         yearMonth: YearMonth,
     ) {
         var currentMonth = yearMonth
         val calendarTable = boxContent.selectFirst("#eventscheduletable")
         var onGoingDay = 1
         var isFirstDay = true
         val onGoingEvents: MutableList<EventsScheduleBuilder.EventEntryBuilder> = mutableListOf()
         for (cell in calendarTable.cells()) {
             val dayDiv = cell.selectFirst("div") ?: throw ParsingException("could not find day's div")
             val day = dayDiv.cleanText().toInt()
             val spans = cell.select("span.HelperDivIndicator")
             // The calendar might start with a day from the previous month
             if (onGoingDay < day) currentMonth = currentMonth.minusMonth()
             if (day < onGoingDay) {
                 currentMonth = currentMonth.plusMonth()
             }
             onGoingDay = day + 1
             val todayEvents: MutableList<EventsScheduleBuilder.EventEntryBuilder> = mutableListOf()
             for (popup in spans) {
                 val (_, popupContent) = parsePopup(popup.attr("onmouseover"))
                 val divs = popupContent.select("div")
                 // Multiple events might be described in the same popup, divs come in pairs, title and content
                 for (index in 0..divs.lastIndex step 2) {
                     val event = eventEntryBuilder {
                         title = divs[index].cleanText().remove(":")
                         description = divs[index + 1].cleanText().remove("• ")
                     }
                     todayEvents.add(event)
                     // If this is not an event that was already ongoing from a previous day, add to list
                     if (!onGoingEvents.contains(event)) {
                         // Only add start date if this is not the first day of the calendar.
                         // If it's the first day, we have no way to know if the event started that day or before
                         if (!isFirstDay) {
                             event.startsOn = LocalDate(currentMonth.year, currentMonth.month, day)
                         }
                         onGoingEvents.add(event)
                     }
                 }
             }
             // Check which of the ongoing events did not show up today, meaning it has ended now
             for (pendingEvent in onGoingEvents.toList()) {
                 if (!todayEvents.contains(pendingEvent)) {
                     // If it didn't show up today, it means it ended yesterday.
                     pendingEvent.endsOn =
                         LocalDate(currentMonth.year, currentMonth.month, day).minus(DatePeriod(days = 1))
                     addEntry(pendingEvent.build())
                     onGoingEvents.remove(pendingEvent)
                 }
             }
             isFirstDay = false
         }
         // Add any leftover ongoing events without an end date, as we don't know when they end.
         onGoingEvents.forEach { addEntry(it.build()) }
     }
 
 }