Files
psu-schedule/ios/Shared/Schedule.swift
Zakhar Timoshenko 170a44c377 Add date and time handling, models, schedule management, storage, and widget support
- Introduced `Dates.swift` for handling date and time in the university's timezone.
- Created `Models.swift` to define data structures for timetable, lessons, and directory search results.
- Implemented `Schedule.swift` for managing lesson slots and timetable functionalities.
- Developed `Storage.swift` for user settings and data persistence.
- Added `Widgets.swift` to create widgets displaying lessons and upcoming classes.
2026-10-18 20:26:12 +03:00

103 lines
4.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// One time slot of a day: parallel lessons of subgroups share it.
struct Slot: Hashable, Identifiable {
var date: CivilDate
var start: ClockTime
var end: ClockTime
var lessons: [Lesson]
var id: String { date.iso + "|" + start.description }
/// "13:40 → 15:05"
var timeLabel: String { "\(start) → \(end)" }
}
func subjectKey(_ subject: String) -> String { subject.trimmed.lowercased() }
/// End of a lesson: its own end, else the bell table's, else 80 minutes after the start.
func lessonEnd(_ lesson: Lesson, _ t: Timetable) -> ClockTime {
if let end = lesson.end.flatMap(ClockTime.init) { return end }
if let bell = t.bells.first(where: { $0.start == lesson.start }), let end = ClockTime(bell.end) { return end }
return (ClockTime(lesson.start) ?? ClockTime(minutes: 0)).adding(minutes: 80)
}
extension Timetable {
func day(_ date: CivilDate) -> Day? { days.first { $0.date == date.iso } }
func week(of date: CivilDate) -> Week? { day(date.monday)?.week ?? day(date)?.week }
var dates: [CivilDate] { days.compactMap { CivilDate($0.date) } }
/// Lessons of a date after the student's subgroup filter and the lessons they hid.
func lessons(on date: CivilDate, subgroup: String?, hidden: Set<String>) -> [Lesson] {
(day(date)?.lessons ?? [])
.filter { subgroup == nil || $0.subgroup == nil || $0.subgroup == subgroup }
.filter { !hidden.contains($0.hideKey) }
}
func slots(on date: CivilDate, subgroup: String?, hidden: Set<String>) -> [Slot] {
let lessons = lessons(on: date, subgroup: subgroup, hidden: hidden)
var order: [String] = []
var groups: [String: [Lesson]] = [:]
for l in lessons {
if groups[l.start] == nil { order.append(l.start) }
groups[l.start, default: []].append(l)
}
return order.compactMap { start in
guard let begin = ClockTime(start), let items = groups[start] else { return nil }
return Slot(date: date, start: begin, end: lessonEnd(items[0], self), lessons: items)
}
.sorted { $0.start < $1.start }
}
/// Palette slot of every subject: alphabetical, so a subject keeps its colour across refreshes.
var subjectSlots: [String: Int] {
let subjects = Set((days.flatMap(\.lessons) + weekly.flatMap(\.lessons)).map { subjectKey($0.subject) }).sorted()
return Dictionary(uniqueKeysWithValues: subjects.enumerated().map { ($1, $0) })
}
}
private let shortWords: Set<String> = ["и", "в", "во", "на", "по", "с", "со", "к", "о", "об", "для", "из", "от"]
/// "Экономика предприятия в машиностроении" → "ЭПМ"; short names stay as they are.
func abbreviate(_ subject: String) -> String {
let name = (subject.split(separator: ":").first.map(String.init) ?? subject).trimmed
if name.count <= 10 { return name }
let letters = name.split(whereSeparator: { " -(),".contains($0) })
.filter { !$0.isEmpty && !shortWords.contains($0.lowercased()) && ($0.first?.isLetter ?? false) }
.map { String($0.first!).uppercased() }
.joined()
return letters.count >= 2 ? String(letters.prefix(4)) : String(name.prefix(8))
}
/// Faculty names as the student knows them.
private let faculties = [
"Финансово-экономический факультет": "ФЭФ",
"Факультет информационных технологий": "ФИТ",
"Факультет компьютерных наук и электроники": "ФКНЭ",
"Гуманитарный факультет": "ГФ",
"Инженерно-строительный факультет": "ИСФ",
"Механико-технологический факультет": "МТФ",
"Юридический факультет": "ЮФ",
]
func facultyShort(_ name: String) -> String? { name.isEmpty ? nil : (faculties[name] ?? name) }
/// What a widget or reminder shows: today's slots, or the next study day's once today is over.
struct Upcoming {
var date: CivilDate
var slots: [Slot]
}
func upcoming(_ t: Timetable, settings: UserSettings, now: Date = Date()) -> Upcoming? {
let today = CivilDate.today(now)
let clock = ClockTime.now(now)
let todays = t.slots(on: today, subgroup: settings.subgroup, hidden: settings.hidden).filter { $0.end > clock }
if !todays.isEmpty { return Upcoming(date: today, slots: todays) }
for step in 1...7 {
let d = today.adding(days: step)
let slots = t.slots(on: d, subgroup: settings.subgroup, hidden: settings.hidden)
if !slots.isEmpty { return Upcoming(date: d, slots: slots) }
}
return nil
}