import Foundation /// Welche Titel oben gross gezeigt werden — und in welcher Reihenfolge. /// /// **Warum es das als eigenen Typ gibt.** Die Auswahl soll sich zufällig /// anfühlen und trotzdem stabil sein: dreimal am Abend dieselbe, am nächsten /// Morgen eine andere. Das ist genau die Art Regel, die man nicht im /// laufenden Betrieb beurteilen kann — „es wechselt zu oft" ist kein Befund, /// den man debuggen könnte. Mit einer Liste hinein und einer heraus schon. /// /// **Der Fehler, der es hierher gebracht hat.** Vorher wurde ein Zufallszahl- /// erzeuger einmal je Zeitfenster aufgesetzt und dann Titel für Titel /// abgefragt. Das Los hing damit an der **Position** in der Liste: kam ein /// Titel dazu, verschwand einer, oder änderte sich der Fortschritt, bekamen /// alle dahinter ein anderes Los — und der Aufmacher war nach jedem Neustart /// ein anderer, obwohl das Zeitfenster dasselbe war. /// /// Jetzt hängt das Los an der **Kennung** des Titels. Wer dazukommt, stört /// niemanden; wer geht, hinterlässt eine Lücke und sonst nichts. public enum HeroSelection { /// Wie lange dieselbe Auswahl gilt. public static let windowHours: Double = 6 /// Ordnet die Titel, ohne die Reihenfolge fest vorzugeben. /// /// **Ungleiche Lose.** Ohne Hintergrundbild ist der Aufmacher eine graue /// Fläche mit Text, und ein Titel ohne eines gehört dort eigentlich nicht /// hin. Ihn auszuschliessen wäre aber zu hart: bei einer Mediathek, in /// der wenige Titel eines tragen, blieben immer dieselben drei übrig. /// Also bekommt er ein schlechteres Los statt gar keines. public static func ordered( _ items: [MediaItem], at time: Date = .now, windowHours: Double = windowHours ) -> [MediaItem] { let fenster = UInt64(max(0, time.timeIntervalSince1970 / (windowHours * 3_600))) var bewertet: [(item: MediaItem, los: Double)] = [] bewertet.reserveCapacity(items.count) for item in items { bewertet.append((item, lot(for: item, window: fenster))) } // Bei gleichem Los die Kennung: zweimal dieselbe Eingabe soll zweimal // dasselbe ergeben, sonst ist ein Fehler nicht wiederholbar. bewertet.sort { links, rechts in links.los == rechts.los ? links.item.id < rechts.item.id : links.los > rechts.los } return bewertet.map(\.item) } /// Das Los eines Titels in diesem Zeitfenster — zwischen 0 und 1. static func lot(for item: MediaItem, window: UInt64) -> Double { let hatBild = item.art != nil || item.seriesArt != nil let gewicht = hatBild ? 1.0 : 0.2 return unitRandom(seed: stableHash(item.id) ^ window) * gewicht } /// Ein Streuwert, der über Programmläufe hinweg gleich bleibt. /// /// **Nicht `hashValue`.** Swift streut Hashes je Programmlauf zufällig; /// damit wäre die Auswahl nach jedem Start eine andere — also genau der /// Fehler, der hier behoben werden soll, nur an einer anderen Stelle. static func stableHash(_ text: String) -> UInt64 { // FNV-1a: vier Zeilen, keine Abhängigkeit, und für das Mischen von // ein paar Kacheln mehr als gut genug. var hash: UInt64 = 0xCBF2_9CE4_8422_2325 for byte in text.utf8 { hash ^= UInt64(byte) hash = hash &* 0x0000_0100_0000_01B3 } return hash } /// SplitMix64, auf 0..<1 gebracht. static func unitRandom(seed: UInt64) -> Double { var z = seed &+ 0x9E37_79B9_7F4A_7C15 z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9 z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB z = z ^ (z >> 31) // Die oberen 53 Bit: genau die, die ein `Double` verlustfrei trägt. return Double(z >> 11) / Double(1 << 53) } }