Повторы - гибкая настройка и проверка


#1

Есть ли какое-то готовое решение для работы с повторами (как в эппловском приложении напоминания)? Необходимо задать настройки повтора и выдавать true/false для определенной даты. При этом частота настраивается также гибко, например: каждый 5 день, каждые 2 недели по вторникам, ежемесячно каждые 10, 20, 30 число, ежемесячно первый понедельник, ежемесячно в последний будний день и тд. Также есть дата начала отсчета и опционально дата окончания.


#2

Разобрался. Если нужно кому-нибудь, пишите - выложу сюда


#3

Подскажите, как вы решили эту задачу?


#4

Реализовал следующим образом, получилось даже более гибко:

extension Date {

    var day: Int {
        return Calendar.current.component(.day, from: self)
    }

    var weekday: Int {
        return (Calendar.current.component(.weekday, from: self) + 7 - Calendar.current.firstWeekday) % 7 + 1
    }
    
    var startOfMonth: Date {
        return Calendar.current.date(from: Calendar.current.dateComponents([.month, .year], from: Calendar.current.startOfDay(for: self)))!
    }

}

func date(day: Int, month: Int, year: Int, hour: Int? = nil, minute: Int? = nil, second: Int? = nil) -> Date {
    return Calendar.current.date(from: DateComponents(year: year, month: month, day: day, hour: hour, minute: minute, second: second)) ?? Date()
}

class Repeat {
    
    enum Month: Int {
        case january = 1, february, march, april, may, june, july, august, september, october, november, december
    }
    
    enum Day: Int {
        case monday = 1, tuesday, wednesday, thursday, friday, saturday, sunday
        case workingDay, dayOff
    }

    enum Index: Int {
        case first, second, third, fourth, fifth, last
    }

    enum Every {
        case day(Int)
        case week(Int, [Day])
        case month(Int, [Int], [(Index, Day)])
        case year(Int, [Month], [(Index, Day)])
    }
    
    var every: Every
    var from: Date
    var till: Date?
    
    init(every: Every, from: Date = Date(), till: Date? = nil) {
        self.every = every
        self.from = Calendar.current.startOfDay(for: from)
        self.till = (till != nil ? Calendar.current.startOfDay(for: till!) : nil)
    }
    
    func indexDayToNumber(_date: Date, index: Index, day: Day) -> Int? {
        
        let dateComponents = Calendar.current.dateComponents([.month, .year], from: _date)
        
        guard let duration = Calendar.current.dateComponents([.day], from: _date.startOfMonth, to: Calendar.current.date(byAdding: DateComponents(month: 1), to: _date.startOfMonth)!).day, let month = dateComponents.month, let year = dateComponents.year else { return nil }
        
        let days = (1 ..< duration + 1).indices.map {
            ($0, date(day: $0, month: month, year: year).weekday)
        }.filter { ($0.1 == day.rawValue) || (day == .workingDay ? (1...5).contains($0.1) : false) || (day == .dayOff ? (6...7).contains($0.1) : false) }
        
        return days[index == .last ? days.count - 1 : index.rawValue].0
    }
    
    func check(date: Date) -> Bool {
        
        let date = Calendar.current.startOfDay(for: date)
        
        guard date >= from, (till != nil ? date <= till! : true) else { return false }
        
        switch every {
        
        case .day(let frequency):
            guard let duration = Calendar.current.dateComponents([.day], from: from, to: date).day else { return false }
            
            return modf(Double(duration) / Double(frequency)).1 == 0
            
        case .week(let frequency, let days):
            guard let duration = Calendar.current.dateComponents([.weekOfYear, .yearForWeekOfYear], from: from, to: date).weekOfYear else { return false }
            
            return modf(Double(duration) / Double(frequency)).1 == 0 && days.contains(where: { $0.rawValue == date.weekday })

        case .month(let frequency, let numbers, let days):
            guard let duration = Calendar.current.dateComponents([.month, .year], from: from, to: date).month else { return false }
            
            let _numbers = days.map {
                indexDayToNumber(_date: date, index: $0.0, day: $0.1)
            }
            
            return modf(Double(duration) / Double(frequency)).1 == 0 && (numbers.contains(date.day) || _numbers.contains(date.day))
        
        case .year(let frequency, let months, let days):
            guard let duration = Calendar.current.dateComponents([.year], from: from, to: date).year, let month = Calendar.current.dateComponents([.month, .year], from: date).month else { return false }
            
            let _numbers = days.map {
                indexDayToNumber(_date: date, index: $0.0, day: $0.1)
            }
            
            return modf(Double(duration) / Double(frequency)).1 == 0 && months.contains(Month(rawValue: month)!) && _numbers.contains(date.day)

        }
    }
}

//Использование:
//let _repeat = Repeat(every: .day(3), from: date(day: 1, month: 3, year: 2021))
//let _repeat = Repeat(every: .week(2, [.monday, .friday]), from: date(day: 1, month: 3, year: 2021))
//let _repeat = Repeat(every: .month(1, [15], [(.first, .monday), (.third, .wednesday), (.last, .workingDay)]), from: date(day: 1, month: 1, year: 2021))
let _repeat = Repeat(every: .year(1, [.january, .march], [(.last, .workingDay)]), from: date(day: 1, month: 1, year: 2021))

for i in 1 ... 3 {
    for j in 1 ... 31 {
        print("\(j).\(i).2021", _repeat.check(date: date(day: j, month: i, year: 2021)))
    }
}