Coverage Summary for Class: WorldOverviewParser (com.galarzaa.tibiakt.core.section.community.world.parser)

Class Class, % Method, % Branch, % Line, % Instruction, %
WorldOverviewParser 100% (1/1) 100% (6/6) 71.9% (23/32) 97.9% (47/48) 88.8% (356/401)


 /*
  * 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.community.world.parser
 
 import com.galarzaa.tibiakt.core.domain.world.BattlEyeType
 import com.galarzaa.tibiakt.core.domain.world.TransferType
 import com.galarzaa.tibiakt.core.enums.StringEnum
 import com.galarzaa.tibiakt.core.exceptions.ParsingException
 import com.galarzaa.tibiakt.core.html.TABLE_SELECTOR
 import com.galarzaa.tibiakt.core.html.parsePopup
 import com.galarzaa.tibiakt.core.parser.Parser
 import com.galarzaa.tibiakt.core.section.community.world.builder.WorldOverviewBuilder
 import com.galarzaa.tibiakt.core.section.community.world.builder.worldOverviewBuilder
 import com.galarzaa.tibiakt.core.section.community.world.model.WorldOverview
 import com.galarzaa.tibiakt.core.text.parseInteger
 import com.galarzaa.tibiakt.core.time.parseTibiaDateTime
 import com.galarzaa.tibiakt.core.time.parseTibiaFullDate
 import org.jsoup.Jsoup
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import org.jsoup.select.Elements
 
 /** Parses content from the world overview section. */
 public object WorldOverviewParser : Parser<WorldOverview> {
     private val recordRegex = Regex("""(?<count>[\d.,]+) players \(on (?<date>[^)]+)\)""")
     private val battlEyeRegex = Regex("""since ([^.]+).""")
 
     override fun fromContent(content: String): WorldOverview {
         val document: Document = Jsoup.parse(content, "", org.jsoup.parser.Parser.xmlParser())
         val boxContent =
             document.selectFirst("div.BoxContent") ?: throw ParsingException("BoxContent container not found")
         val tables = boxContent.select(TABLE_SELECTOR)
         val builder = worldOverviewBuilder {
             recordRegex.find(tables.first()?.text() ?: throw ParsingException("No content tables found."))?.apply {
                 val (_, recordCount, recordDate) = groupValues
                 overallMaximumCount = recordCount.parseInteger()
                 overallMaximumCountDateTime = parseTibiaDateTime(recordDate)
             }
             for (i in 1..tables.lastIndex step 2) {
                 val headerTable = tables[i]
                 val worldsTable = tables[i + 1]
                 if (headerTable.text().contains("Regular")) parseWorldsTable(worldsTable)
             }
         }
         return builder.build()
     }
 
     private fun WorldOverviewBuilder.parseWorldsTable(table: Element) {
         val rows = table.select("tr")
         for (row in rows.subList(1, rows.size)) {
             addWorld {
                 val columns = row.select("td")
                 name = columns.first()?.text() ?: throw ParsingException("No columns in world row.")
                 parseOnlineStatus(columns)
                 location = columns[2].text()
                 pvpType = StringEnum.fromValue(columns[3].text())
                     ?: throw ParsingException("Unknown PvP type found: ${columns[3].text()}")
 
                 parseBattlEyeStatus(columns)
                 val additionalProperties = columns[5].text()
                 transferType = if (additionalProperties.contains("blocked")) {
                     TransferType.BLOCKED
                 } else if (additionalProperties.contains("locked")) {
                     TransferType.LOCKED
                 } else {
                     TransferType.REGULAR
                 }
                 isPremiumRestricted = additionalProperties.contains("premium")
                 isExperimental = additionalProperties.contains("experimental")
             }
         }
     }
 
     private fun WorldOverviewBuilder.WorldEntryBuilder.parseBattlEyeStatus(columns: Elements) {
         battlEyeType = BattlEyeType.UNPROTECTED
         columns[4].selectFirst("span.HelperDivIndicator")?.apply {
             val (_, popUp) = parsePopup(attr("onmouseover"))
             battlEyeRegex.find(popUp.text())?.apply {
                 try {
                     battlEyeStartedOn = parseTibiaFullDate(groups[1]!!.value)
                 } catch (e: IllegalArgumentException) {
                     // Leave value as none
                 }
             }
             battlEyeType = if (battlEyeStartedOn == null) BattlEyeType.GREEN else BattlEyeType.YELLOW
         }
     }
 
     private fun WorldOverviewBuilder.WorldEntryBuilder.parseOnlineStatus(columns: Elements) {
         try {
             onlinePlayersCount = columns[1].text().parseInteger()
             isOnline = true
         } catch (nfe: NumberFormatException) {
             onlinePlayersCount = 0
             isOnline = false
         }
     }
 }