<commit_before>package ru.dyatel.tsuschedule.parsing

import org.jsoup.nodes.Element
import ru.dyatel.tsuschedule.EmptyResultException
import ru.dyatel.tsuschedule.ParsingException
import ru.dyatel.tsuschedule.data.BaseLesson
import ru.dyatel.tsuschedule.data.LessonType
import ru.dyatel.tsuschedule.data.Parity
import ru.dyatel.tsuschedule.utilities.NullableLateinit
import java.util.HashSet

abstract class BaseParser<out T : BaseLesson> {

    private companion object {
        val WEEKDAY_PATTERN = Regex("\\b[А-Яа-я]+\\b")
    }

    fun parse(element: Element): Set<T> {
        if (element.childNodeSize() <= 1) {
            throw EmptyResultException()
        }

        val lessons = HashSet<T>()
        var currentWeekday: String? = null

        element.children()
                .filter { !it.hasClass("screenonly") }
                .map { it.child(0).child(0).children().last() } // Ignore the padding row
                .forEach {
                    val builder = BaseLessonBuilder()

                    val timeText = it.getElementsByClass("time").requireSingle().text().trim()
                    val weekday = WEEKDAY_PATTERN.find(timeText)?.value ?: currentWeekday
                    currentWeekday = weekday ?: throw ParsingException("Can't find weekday of the lesson")

                    builder.setWeekday(weekday)
                    builder.parseParity(it.getElementsByClass("parity").requireSingle().text())
                    builder.parseTime(timeText)

                    builder.parseDescription(it.getElementsByClass("disc").requireSingle().text())
                    builder.parseAuditory(it.getElementsByClass("aud").requireSingleOrNull()?.text())

                    lessons += parseSingle(it, builder.build())
                }

        return lessons
    }

    protected abstract fun parseSingle(e: Element, base: BaseLesson): T

    protected fun <T> Collection<T>.requireSingle() = singleOrNull() ?: throw ParsingException()

    protected fun <T> Collection<T>.requireSingleOrNull() = if (isEmpty()) null else requireSingle()

}

private class BaseLessonBuilder {

    private companion object {
        val BLANK_PARENTHESES_PATTERN = Regex("\\(\\s*\\)")

        val TIME_PATTERN = Regex("\\d{2}:\\d{2}-\\d{2}:\\d{2}")

        val TYPE_MAPPING = mapOf(
                "Пр" to LessonType.PRACTICE,
                "Практ" to LessonType.PRACTICE,
                "Л" to LessonType.LECTURE,
                "Лаб" to LessonType.LABORATORY
        )
        val TYPE_PATTERN = TYPE_MAPPING.keys.joinToString("|", "\\((", ")\\.?\\)").toRegex()
    }

    private lateinit var parity: Parity

    private lateinit var weekday: String
    private lateinit var time: String

    private lateinit var discipline: String
    private var auditory by NullableLateinit<String>()

    private lateinit var type: LessonType

    fun parseParity(text: String) {
        parity = when (text.trim()) {
            "н/н" -> Parity.ODD
            "ч/н" -> Parity.EVEN
            else -> throw ParsingException("Can't determine parity from <$text>")
        }
    }

    fun setWeekday(text: String) {
        weekday = text
    }

    fun parseTime(text: String) {
        time = TIME_PATTERN.find(text.trim())?.value
                ?: throw ParsingException("Can't parse lesson's time from <$text>")
    }

    fun parseDescription(text: String) {
        text.replace(BLANK_PARENTHESES_PATTERN, "")
                .let {
                    val match = TYPE_PATTERN.find(it)
                    if (match != null) {
                        type = TYPE_MAPPING[match.groupValues[1]]!!
                        it.removeRange(match.range)
                    } else {
                        type = LessonType.UNKNOWN
                        it
                    }
                }
                .trim()
                .removeSuffix(",").trim()
                .let { discipline = it }
    }

    fun parseAuditory(text: String?) {
        auditory = text?.trim()?.takeUnless { it.isEmpty() }
    }

    fun build() = BaseLesson(parity, weekday, time, discipline, auditory, type)

}<commit_msg>Get rid of BaseLessonBuilder<commit_after>package ru.dyatel.tsuschedule.parsing

import org.jsoup.nodes.Element
import ru.dyatel.tsuschedule.EmptyResultException
import ru.dyatel.tsuschedule.ParsingException
import ru.dyatel.tsuschedule.data.BaseLesson
import ru.dyatel.tsuschedule.data.LessonType
import ru.dyatel.tsuschedule.data.Parity
import java.util.HashSet

abstract class BaseParser<out T : BaseLesson> {

    private companion object {
        val WEEKDAY_PATTERN = Regex("\\b[А-Яа-я]+\\b")
        val TIME_PATTERN = Regex("\\d{2}:\\d{2}-\\d{2}:\\d{2}")
        val BLANK_PARENTHESES_PATTERN = Regex("\\(\\s*\\)")

        val TYPE_MAPPING = mapOf(
                "Пр" to LessonType.PRACTICE,
                "Практ" to LessonType.PRACTICE,
                "Л" to LessonType.LECTURE,
                "Лаб" to LessonType.LABORATORY
        )
        val TYPE_PATTERN = TYPE_MAPPING.keys.joinToString("|", "\\((", ")\\.?\\)").toRegex()
    }

    fun parse(element: Element): Set<T> {
        if (element.childNodeSize() <= 1) {
            throw EmptyResultException()
        }

        val lessons = HashSet<T>()
        var currentWeekday: String? = null

        element.children()
                .filter { !it.hasClass("screenonly") }
                .map { it.child(0).child(0).children().last() } // Ignore the padding row
                .forEach {
                    val timeText = it.getElementsByClass("time").requireSingle().text().trim()
                    val weekday = WEEKDAY_PATTERN.find(timeText)?.value ?: currentWeekday
                    currentWeekday = weekday ?: throw ParsingException("Can't find weekday of the lesson")

                    val parity = parseParity(it)
                    val time = parseTime(timeText)
                    val auditory = parseAuditory(it)
                    val (type, discipline) = parseDiscipline(it)

                    lessons += parseSingle(it, BaseLesson(parity, weekday, time, discipline, auditory, type))
                }

        return lessons
    }

    private fun parseParity(e: Element): Parity {
        val text = e.getElementsByClass("parity").requireSingle().text().trim()
        return when (text) {
            "н/н" -> Parity.ODD
            "ч/н" -> Parity.EVEN
            else -> throw ParsingException("Can't determine parity from <$text>")
        }
    }

    private fun parseTime(text: String): String {
        return TIME_PATTERN.find(text)?.value ?: throw ParsingException("Can't parse time from <$text>")
    }

    private fun parseAuditory(e: Element): String? {
        val text = e.getElementsByClass("aud").requireSingleOrNull()?.text()?.trim()
        return text?.takeUnless { it.isEmpty() }
    }

    private fun parseDiscipline(e: Element): Pair<LessonType, String> {
        var text = e.getElementsByClass("disc").requireSingle().text().trim()
                .removeSuffix(",")
                .replace(BLANK_PARENTHESES_PATTERN, "")

        val typeMatch = TYPE_PATTERN.find(text)
        val type: LessonType?

        if (typeMatch != null) {
            type = TYPE_MAPPING[typeMatch.groupValues[1]]!!
            text = text.removeRange(typeMatch.range)
        } else {
            type = LessonType.UNKNOWN
        }

        return type to text.trim()
    }

    protected abstract fun parseSingle(e: Element, base: BaseLesson): T

    protected fun <T> Collection<T>.requireSingle() = singleOrNull() ?: throw ParsingException()

    protected fun <T> Collection<T>.requireSingleOrNull() = if (isEmpty()) null else requireSingle()

}
{'fraction_non_alphanumeric': 0.12183729077462048, 'fraction_numerical': 0.0020760347735824577, 'mean_word_length': 2.142274765593151, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5561466', 'n_tokens_mistral': 2350, 'n_tokens_neox': 2226, 'n_words': 461}