0
\$\begingroup\$

A Kotlin class that I wrote fairly early on while learning Kotlin which is used by another program to build and generate HTML visualisations of timetables. I'd be grateful for any feedback, especially if there's any way (that's not too verbose) of tidying up the multilayer data structure.

import data.Session
import data.Slot
import java.io.PrintWriter

// Boolean is true if this is the first slot for this session, as this is where the HTML tag should be placed.
typealias Cell = Pair<MutableList<Session>?, Boolean>
typealias Grid = Array<Array<Cell>>
typealias Day = MutableList<Grid>
typealias Semester = Array<Day>

class HTMLTimetable {

    val sessions: Array<Semester> = Array(2) {
        // 2 semesters
        Array(5) {
            // 5 days
            MutableList(1) {
                // Variable number of layers
                Array(12) {
                    // 12 weeks
                    Array(12) {
                        // 12 slots
                        Pair(null as MutableList<Session>?, false)
                    }
                }
            }
        }
    }

    fun newGrid(): Grid = Array(12) { Array(12) { Pair(null as MutableList<Session>?, false)}}

    fun addSession(newSession : Session, weekFilter : (Int) -> Boolean) {
        var tryingLayer = 0
        var thisLayerOK = false
        var mergePossible = false

        var mergeDebug: Session? = null
        var mergeDebugSlot : Cell? = null
        var mergeDebugLayer = 0
        var mergeDebugWeek = 0
        var mergeDebugOrd = 0


        // Check a slot exists.
        if (newSession.slot == null) {
            println("Trying to HTML timetable a session with no slot, $newSession")
            return
        }

        // Find a layer with free space in the appropriate position
        val semDayLayerArray = sessions[newSession.semester-1][newSession.slot!!.day]
        while (!thisLayerOK) {
            thisLayerOK = true
            // See if there's any week in which this layer is blocked
            // This makes sure that all weeks of a module will be on the same layer
            // In each week the module runs..
            checkFreeLayerLoop@ for (week in (0..11).filter {newSession.runsInWeek(it) && weekFilter(it)} ) {
                val tryingWeekArray = semDayLayerArray[tryingLayer][week]
                // For each slot the module occupies in those weeks..
                val startSlot = newSession.slot!!.ordinalInDay
                val endSlot = newSession.lastSlot.ordinalInDay
                for (daySlot in startSlot..endSlot) {
                    val tryingSlot = tryingWeekArray[daySlot]
                    // Is slot occupied?
                    if (tryingSlot.first != null) {
                        // If the session we're adding is already there, don't add it again at all
                        if (tryingSlot.first!!.contains(newSession)) return
                        // There's a session there, maybe it's mergable though?
                        val existingSession = tryingSlot.first!![0]
                        if ((existingSession.module == newSession.module) || (existingSession.isMergedWith(newSession))) {
                            if (existingSession.type == newSession.type) {
                                if (existingSession.slot == newSession.slot) {
                                    if (existingSession.length == newSession.length) {
                                        // println("Merge possible between $existingSession and $newSession triggered by $tryingSlot")
                                        mergePossible = true
                                        mergeDebugSlot = tryingSlot
                                        mergeDebug = existingSession
                                        mergeDebugLayer = tryingLayer
                                        mergeDebugWeek = week
                                        mergeDebugOrd = daySlot
                                        continue@checkFreeLayerLoop
                                    }
                                }
                            }
                        }
                        // Slot is occupied and merge not possible. Break checking loop and go to next layer
                        mergePossible = false
                        thisLayerOK = false
                        break@checkFreeLayerLoop
                    }
                }
            }
            // If loop was broken without finding a layer, try next layer
            if (!thisLayerOK) {
                tryingLayer++
                // If that was the last layer, add another layer, then add event to that
                // (Since the new layer will be empty, it will certainly be unblocked)
                if (tryingLayer > semDayLayerArray.lastIndex) {
                    semDayLayerArray.add(newGrid())
                    thisLayerOK = true
                    mergePossible = false
                }
            }
        }

        // thisLayerOK should now be true.
        // mergePossible: layer has existing sessions in these slots which are mergable.
        //

        var isFirst : Boolean

        for (week in (0..11).filter {newSession.runsInWeek(it) && weekFilter(it)}) {
            isFirst = true
            for (dayOrdinal in (newSession.slot!!.ordinalInDay)..(newSession.slot!!.ordinalInDay + newSession.length - 1)) {
                if (mergePossible) {
                    if (semDayLayerArray[tryingLayer][week][dayOrdinal].first == null) {
                        // This can occur in the following circumstances:
                        // A module is added in an earlier pass with a week filter that includes only some earlier sessions.
                        // Then added again in a later pass (eg, staff as well as room clash) with a more permissive week filter.
                        // It is identified as a merge based on the earlier sessions, but then the later sessions have no
                        // merge target, because even though the session runs in the later weeks, they were not previously
                        // added due to the week filter.
                        semDayLayerArray[tryingLayer][week][dayOrdinal] = Pair(mutableListOf(newSession), isFirst)
                        isFirst = false
                        continue
                    }
                    semDayLayerArray[tryingLayer][week][dayOrdinal].first!!.add(newSession)
                } else {
                    semDayLayerArray[tryingLayer][week][dayOrdinal] = Pair(mutableListOf(newSession), isFirst)
                    isFirst = false
                }
            }
        }

    }


    fun output(target: PrintWriter, header: String, colorizer: (MutableList<Session>) -> String) {
        target.println("<html><head><title>$header</title></head><body style=\"font-family: sans-serif\"><h1>$header</h1>")
        for (semester in 0..1) {
            for (day in 0..4) {
                val semDayLayerArray = sessions[semester][day]
                // If no sessions on this day/semester, stop
                if (semDayLayerArray[0].all {
                            it.all {
                                it.first == null
                            }
                        })
                    continue

                target.println("<h3>${Slot.dayNames[day]}, Semester ${semester+1} </h3>")
                val maxLayer = semDayLayerArray.size
                val colSpan = when {
                    maxLayer == 1 -> 1
                    else -> maxLayer + 1
                }
                target.println("<table><tr><th></th>")
                for (headerWeek in 0..11) {
                    target.println("<th colspan=$colSpan>Week ${headerWeek+1} </th>")
                }
                target.println("</tr>")
                for (slot in 0..11) {
                    target.println("<tr>")
                    target.println("<th>${slot+9} - ${slot+10}</th>")
                    for (week in 0..11) {
                        for (layer in semDayLayerArray) {
                            val thisSlot = layer[week][slot]
                            if ((thisSlot.second) && (thisSlot.first != null)) {
                                val thisSessions = thisSlot.first!!
                                val bgcol = colorizer(thisSessions)
                                target.println("<td style=\"border: 2px solid black; background-color: $bgcol\" rowspan=${thisSessions[0].length}>")
                                val names = thisSessions.map { "${it.module.name} ${it.module.fullName} (${it.module.level})" }.distinct().toString()
                                target.println("$names<br>${thisSlot.first!![0].type}<br>")
                                if (thisSlot.first!!.size > 1) {
                                    target.println("${thisSlot.first!!.size} sets<br>")
                                    for (session in thisSlot.first!!) {
                                        target.print("${session.room!!.name}: ")
                                        session.staffLoadingByWeek[week].forEach { (staffer, int) ->
                                            target.print("${staffer.name} ")
                                        }
                                        target.println("<br>")
                                    }
                                } else {
                                    target.println("${thisSlot.first!![0].room!!.name}<br>")
                                    if (!thisSlot.first!![0].mandatory) {
                                        target.println("${thisSlot.first!![0].altThread + 1} / ${thisSlot.first!![0].altThreadMax}<br>")
                                    }
                                    thisSlot.first!![0].staffLoadingByWeek[week].forEach { (staffer, int) ->
                                        target.println("${staffer.name} <br>")
                                    }
                                }
                                target.println("</td>")
                            } else if (thisSlot.first == null) {
                                target.println("<td style=\"border: 1px solid gray;\"></td>")
                            }
                        }
                        if (maxLayer > 1) {
                            target.println("<td style=\"border: 1px solid black; background-color: black;\"></td>")
                        }
                    }
                    target.println("</tr>")
                }
                target.println("</table><br><br>")

            }
        }
        target.println("</body></html>")
        target.flush()
    }

}
\$\endgroup\$
4
  • 1
    \$\begingroup\$ is newSession.slot!!.ordinalInDay + newSession.length - 1 the same as newSession.lastSlot.ordinalInDay? \$\endgroup\$ – tieskedh Sep 12 '19 at 7:58
  • \$\begingroup\$ Good point, it should be. \$\endgroup\$ – Mark Green Sep 12 '19 at 16:49
  • \$\begingroup\$ another question. I like the excelrcise, so even if you are ready, please provide them... I like to have te Session and the slot-classes. (or at least without implementation, but the var/val such that i know which is nullable etc.) \$\endgroup\$ – tieskedh Oct 5 '19 at 15:06
  • \$\begingroup\$ One little question, do you need mergeDebug, mergeDebugSlot, mergeDebugLayer, mergeDebugWeek, mergeDebugOrd at the top layer where you have define them? Thats the part which makes it challenging. Simplifying the rest is easy... \$\endgroup\$ – tieskedh Oct 5 '19 at 17:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.