├── .DS_Store ├── .swiftpm └── xcode │ └── xcuserdata │ └── jichanpark.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── .DS_Store ├── Builder │ └── PropertyBuilder.swift ├── Component │ ├── .DS_Store │ ├── ICalAlarm.swift │ ├── ICalComponent.swift │ ├── ICalEvent.swift │ ├── ICalSubTimeZone.swift │ ├── ICalTimeZone.swift │ └── ICalendar.swift ├── Constant │ └── Constant.swift ├── Extension │ ├── Bool+VPropertyEncodable.swift │ ├── Date+VPropertyEncodable.swift │ ├── Int+VPropertyEncodable.swift │ ├── String+Utilities.swift │ ├── String+VPropertyEncodable.swift │ ├── URL+VPropertyEncodable.swift │ └── UUID+VPropertyEncodable.swift ├── Parser │ └── ICalParser.swift ├── Protocol │ ├── VComponent.swift │ ├── VEncodable.swift │ └── VPropertyEncodable.swift ├── Structure │ ├── ICalAttachment.swift │ ├── ICalDateTime.swift │ ├── ICalDateTimes.swift │ ├── ICalDuration.swift │ ├── ICalParameter.swift │ ├── ICalPeriod.swift │ ├── ICalProductIdentifier.swift │ ├── ICalRRule.swift │ └── VContentLine.swift └── Utils │ └── DateTimeUtil.swift └── Tests └── ICalSwiftTests └── ICalSwiftTests.swift /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chan614/iCalSwift/b2e1161d186fcd78b36875bee0d02df3e9e193c0/.DS_Store -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/jichanpark.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ICalSwift.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 7 11 | 12 | MpICalendarKit.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 7 16 | 17 | iCalSwift.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 20 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 chan614 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ICalSwift", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "ICalSwift", 12 | targets: ["ICalSwift"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "ICalSwift", 23 | dependencies: [], 24 | path: "Sources"), 25 | .testTarget( 26 | name: "ICalSwiftTests", 27 | dependencies: ["ICalSwift"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iCalSwift 2 | 3 | [iCalendar(RFC 5545)](https://tools.ietf.org/html/rfc5545#section-3.8.7.4) encoder and decoder for Swift 4 | 5 | ## Encode a VEvent 6 | 7 | ```swift 8 | let alarm = ICalAlarm.audioProp( 9 | trigger: Date(), 10 | duration: .init(totalSeconds: 3000), 11 | repetition: nil, 12 | attach: nil) 13 | 14 | let event = ICalEvent( 15 | dtstamp: Date(), 16 | uid: "example@gmail.com", 17 | classification: nil, 18 | created: Date(), 19 | description: "example", 20 | dtstart: .init(date: Date()), 21 | lastModified: Date(), 22 | location: "1", 23 | organizer: nil, 24 | priority: 1, 25 | seq: nil, 26 | status: "CONFIRMED", 27 | summary: "Spinning", 28 | transp: "SPAQUE", 29 | url: nil, 30 | dtend: nil, 31 | duration: nil, 32 | recurrenceID: Date(), 33 | rrule: nil, 34 | rdates: [Date(), Date(), Date()], 35 | exrule: nil, 36 | exdates: [Date(), Date()], 37 | alarms: [alarm], 38 | timeZone: nil, 39 | extendProperties: ["X-EXTEND-PROPERTY": "TEST"]) 40 | 41 | let vEncoded = event.vEncoded 42 | 43 | print(vEncoded) 44 | ``` 45 | 46 | This will encode a `VEvent` to 47 | 48 | ``` 49 | BEGIN:VEVENT 50 | DTSTAMP:20220305T092707Z 51 | UID:example@gmail.com 52 | CREATED:20220305T092707Z 53 | DESCRIPTION:example 54 | DTSTART:20220305T092707Z 55 | LAST-MODIFIED:20220305T092707Z 56 | LOCATION:1 57 | PRIORITY:1 58 | STATUS:CONFIRMED 59 | SUMMARY:Spinning 60 | TRANSP:SPAQUE 61 | RECURRENCE-ID:20220305T092707Z 62 | RRULE:FREQ=DAILY;INTERVAL=30;COUNT=3;BYMINUTE=10,30;BYDAY=1FR;WKST=SU 63 | RDATE:20220305T092707Z 64 | RDATE:20220305T092707Z 65 | RDATE:20220305T092707Z 66 | EXDATE:20220305T092707Z 67 | EXDATE:20220305T092707Z 68 | X-EXTEND-PROPERTY:TEST 69 | BEGIN:VALAM 70 | ACTION:AUDIO 71 | TRIGGER:20220305T092707Z 72 | DURATION:P0DT0H50M0S 73 | END:VALAM 74 | END:VEVENT 75 | ``` 76 | 77 | ## Decode a VEvent 78 | 79 | ```swift 80 | let sampleICS = """ 81 | BEGIN:VEVENT 82 | DTSTAMP:20220305T092707 83 | UID:example@gmail.com 84 | ... 85 | END:VALAM 86 | END:VEVENT 87 | """ 88 | 89 | let parser = ICalParser() 90 | let vEvents = parser.parseEvent(ics: sampleICS) 91 | 92 | vEvents.forEach { vEvent in 93 | print(vEvent.vEncoded) 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /Sources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chan614/iCalSwift/b2e1161d186fcd78b36875bee0d02df3e9e193c0/Sources/.DS_Store -------------------------------------------------------------------------------- /Sources/Builder/PropertyBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyBuilder.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct PropertyBuilder { 10 | 11 | /// Duration property 12 | static func buildDuration(value: String) -> ICalDuration { 13 | let weeksStr = matcheDuration(type: "W", duration: value) 14 | let daysStr = matcheDuration(type: "D", duration: value) 15 | let hoursStr = matcheDuration(type: "H", duration: value) 16 | let minutesStr = matcheDuration(type: "M", duration: value) 17 | let secondsStr = matcheDuration(type: "S", duration: value) 18 | 19 | let weeks = Int(weeksStr) ?? .zero 20 | let days = Int(daysStr) ?? .zero 21 | let hours = Int(hoursStr) ?? .zero 22 | let minutes = Int(minutesStr) ?? .zero 23 | let seconds = Int(secondsStr) ?? .zero 24 | 25 | return .init(weeks: weeks, days: days, hours: hours, minutes: minutes, seconds: seconds) 26 | } 27 | 28 | /// Recurrence Rule Property 29 | static func buildRRule(value: String) -> ICalRRule? { 30 | let params = paramsOfValue(value) 31 | let frequencyProperty = params 32 | .filter { $0.name == Constant.Prop.frequency } 33 | .first 34 | 35 | guard let frequencyProperty = frequencyProperty, 36 | let frequency = ICalRRule.Frequency(rawValue: frequencyProperty.value) 37 | else { 38 | return nil 39 | } 40 | 41 | var rule = ICalRRule(frequency: frequency) 42 | 43 | params.forEach { property in 44 | switch property.name { 45 | case Constant.Prop.interval: 46 | rule.interval = Int(property.value) 47 | case Constant.Prop.until: 48 | rule.until = buildDateTime(propName: property.name, value: property.value) 49 | case Constant.Prop.count: 50 | rule.count = Int(property.value) 51 | case Constant.Prop.bySecond: 52 | rule.bySecond = separateCommaProperty(value: property.value).compactMap { Int($0) } 53 | case Constant.Prop.byMinute: 54 | rule.byMinute = separateCommaProperty(value: property.value).compactMap { Int($0) } 55 | case Constant.Prop.byHour: 56 | rule.byHour = separateCommaProperty(value: property.value).compactMap { Int($0) } 57 | case Constant.Prop.byDay: 58 | rule.byDay = separateCommaProperty(value: property.value).compactMap { .from($0) } 59 | case Constant.Prop.byDayOfMonth: 60 | rule.byDayOfMonth = separateCommaProperty(value: property.value).compactMap { Int($0) } 61 | case Constant.Prop.byDayOfYear: 62 | rule.byDayOfYear = separateCommaProperty(value: property.value).compactMap { Int($0) } 63 | case Constant.Prop.byWeekOfYear: 64 | rule.byWeekOfYear = separateCommaProperty(value: property.value).compactMap { Int($0) } 65 | case Constant.Prop.byMonth: 66 | rule.byMonth = separateCommaProperty(value: property.value).compactMap { Int($0) } 67 | case Constant.Prop.bySetPos: 68 | rule.bySetPos = separateCommaProperty(value: property.value).compactMap { Int($0) } 69 | case Constant.Prop.startOfWorkweek: 70 | rule.startOfWorkweek = .init(rawValue: property.value) 71 | default: 72 | break 73 | } 74 | } 75 | 76 | return rule 77 | } 78 | 79 | /// DateTime / Date property 80 | static func buildDateTime(propName: String, value: String) -> ICalDateTime? { 81 | let params = paramsOfValue(propName) 82 | let valueType = dateValueType(params: params) 83 | let tzid = timeZoneID(params: params) 84 | 85 | guard let date = DateTimeUtil.dateFormatter(type: valueType, tzid: tzid).date(from: value) else { 86 | return nil 87 | } 88 | 89 | switch valueType { 90 | case .date: 91 | return .dateOnly(date) 92 | default: 93 | return .dateTime(date, tzid: tzid) 94 | } 95 | } 96 | 97 | static func buildAttachment(propName: String, value: String) -> ICalAttachment? { 98 | let params: [ICalParameter] = paramsOfValue(propName) 99 | .map { .init(key: $0.name, values: [$0.value]) } 100 | 101 | return .init(parameters: params, value: value) 102 | } 103 | 104 | static func buildDateTimes(propName: String, value: String) -> ICalDateTimes? { 105 | let params = paramsOfValue(propName) 106 | let valueType = dateValueType(params: params) 107 | let tzid = timeZoneID(params: params) 108 | 109 | let periods = [ICalPeriod]() 110 | let dates = value 111 | .components(separatedBy: ",") 112 | .compactMap { 113 | DateTimeUtil.dateFormatter(type: valueType, tzid: tzid).date(from: $0) 114 | } 115 | 116 | if dates.isEmpty && periods.isEmpty { 117 | return nil 118 | } 119 | 120 | switch valueType { 121 | case .date: 122 | return .dateOnly(dates) 123 | case .dateTime: 124 | return .dateTime(dates, tzid: tzid) 125 | case .period: 126 | // TODO 127 | return .period([], tzid: tzid) 128 | } 129 | } 130 | 131 | // MARK: - Supporting function 132 | 133 | private static func findProperty( 134 | name: String, 135 | elements: [(name: String, value: String)] 136 | ) -> (name: String, value: String)? { 137 | return elements 138 | .filter { $0.name.hasPrefix(name) } 139 | .first 140 | } 141 | 142 | private static func paramsOfValue(_ value: String) -> [(name: String, value: String)] { 143 | return value.components(separatedBy: ";") 144 | .map { $0.components(separatedBy: "=") } 145 | .filter { $0.count > 1 } 146 | .map { ($0[0], $0[1]) } 147 | } 148 | 149 | private static func separateCommaProperty(value: String) -> [String] { 150 | return value.components(separatedBy: ",") 151 | } 152 | 153 | private static func matcheDuration(type: String, duration: String) -> String { 154 | do { 155 | let pattern = "[0-9]+\(type)" 156 | let regex = try NSRegularExpression(pattern: pattern, options: []) 157 | let nsString = NSString(string: duration) 158 | let results = regex.matches( 159 | in: duration, 160 | options: [], 161 | range: NSRange(location: 0, length: nsString.length)) 162 | 163 | return results 164 | .map { nsString.substring(with: $0.range) } 165 | .map { String($0.prefix($0.count - 1)) } 166 | .first ?? "" 167 | } catch { 168 | return "" 169 | } 170 | } 171 | 172 | private static func dateValueType(params: [(name: String, value: String)]) -> DateValueType { 173 | let valueType = params.first { $0.name == "VALUE" }?.value ?? "" 174 | 175 | switch valueType { 176 | case "DATE": 177 | return .date 178 | case "PERIOD": 179 | return .period 180 | default: 181 | return .dateTime 182 | } 183 | } 184 | 185 | private static func timeZoneID(params: [(name: String, value: String)]) -> String? { 186 | return params.first { $0.name == "TZID" }?.value 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Sources/Component/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chan614/iCalSwift/b2e1161d186fcd78b36875bee0d02df3e9e193c0/Sources/Component/.DS_Store -------------------------------------------------------------------------------- /Sources/Component/ICalAlarm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalAlarm.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | /// Provide a grouping of component properties that define an 10 | /// alarm. 11 | /// 12 | /// See https://tools.ietf.org/html/rfc5545#section-3.6.6 13 | public struct ICalAlarm: VComponent { 14 | public let component = Constant.Component.alarm 15 | 16 | /// This property defines the action to be invoked when an 17 | /// alarm is triggered. 18 | /// 19 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.6.1 20 | public var action: String 21 | 22 | /// This property specifies when an alarm will trigger. 23 | /// 24 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.6.3 25 | public var trigger: Date 26 | 27 | /// This property defines a short summary or subject for the 28 | /// calendar component. 29 | /// 30 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.12 31 | public var summary: String? 32 | 33 | /// This property provides a more complete description of the 34 | /// calendar component than that provided by the "SUMMARY" property. 35 | /// 36 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.5 37 | public var description: String? 38 | 39 | /// This value type is used to identify properties that contain 40 | /// a duration of time. 41 | /// 42 | /// See https://tools.ietf.org/html/rfc5545#section-3.3.6 43 | public var duration: ICalDuration? 44 | 45 | /// This property defines the number of times the alarm should 46 | /// be repeated, after the initial trigger. 47 | /// /// 48 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.6.2 49 | public var repetition: Int? 50 | 51 | /// This property provides the capability to associate a 52 | /// document object with a calendar component. 53 | /// 54 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.1 55 | public var attach: String? 56 | 57 | public var properties: [VContentLine?] { 58 | [ 59 | .line(Constant.Prop.action, action), 60 | .line(Constant.Prop.trigger, trigger), 61 | .line(Constant.Prop.description, description), 62 | .line(Constant.Prop.summary, summary), 63 | .line(Constant.Prop.duration, duration), 64 | .line(Constant.Prop.repetition, repetition), 65 | .line(Constant.Prop.attach, attach) 66 | ] 67 | } 68 | 69 | public static func audioProp( 70 | trigger: Date, 71 | duration: ICalDuration? = nil, 72 | repetition: Int? = nil, 73 | attach: String? = nil 74 | ) -> ICalAlarm { 75 | return .init( 76 | action: "AUDIO", 77 | trigger: trigger, 78 | summary: nil, 79 | description: nil, 80 | duration: duration, 81 | repetition: repetition, 82 | attach: attach) 83 | } 84 | 85 | public static func displayProp( 86 | trigger: Date, 87 | description: String, 88 | duration: ICalDuration? = nil, 89 | repetition: Int? = nil 90 | ) -> ICalAlarm { 91 | return .init( 92 | action: "DISPLAY", 93 | trigger: trigger, 94 | summary: nil, 95 | description: description, 96 | duration: duration, 97 | repetition: repetition, 98 | attach: nil) 99 | } 100 | 101 | public static func emailProp( 102 | trigger: Date, 103 | description: String, 104 | summary: String, 105 | duration: ICalDuration? = nil, 106 | repetition: Int? = nil, 107 | attach: String? = nil 108 | ) -> ICalAlarm { 109 | return .init( 110 | action: "EMAIL", 111 | trigger: trigger, 112 | summary: summary, 113 | description: description, 114 | duration: duration, 115 | repetition: repetition, 116 | attach: attach) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/Component/ICalComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalComponent.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | public struct ICalComponent { 10 | let properties: [(name: String, value: String)] 11 | let children: [(name: String, value: String)] 12 | 13 | func findProperty(name: String) -> (name: String, value: String)? { 14 | return properties 15 | .filter { $0.name.hasPrefix(name) } 16 | .first 17 | } 18 | 19 | func findProperties(name: String) -> [(name: String, value: String)]? { 20 | return properties.filter { $0.name.hasPrefix(name) } 21 | } 22 | 23 | func findExtendProperties() -> [String: String] { 24 | var dict = [String: String]() 25 | 26 | properties 27 | .filter { $0.name.hasPrefix("X-") } 28 | .forEach { dict[$0.name] = $0.value } 29 | 30 | return dict 31 | } 32 | 33 | // DateTime 34 | func buildProperty(of name: String) -> ICalDateTime? { 35 | guard let prop = findProperty(name: name) else { 36 | return nil 37 | } 38 | 39 | return PropertyBuilder.buildDateTime(propName: prop.name, value: prop.value) 40 | } 41 | 42 | // Int 43 | func buildProperty(of name: String) -> Int? { 44 | guard let prop = findProperty(name: name) else { 45 | return nil 46 | } 47 | 48 | return Int(prop.value) 49 | } 50 | 51 | // String 52 | func buildProperty(of name: String) -> String? { 53 | guard let prop = findProperty(name: name) else { 54 | return nil 55 | } 56 | 57 | return prop.value 58 | .replacing(pattern: "\\\\,", with: ",") 59 | .replacing(pattern: "\\\\;", with: ";") 60 | .replacing(pattern: "\\\\[nN]", with: "\n") 61 | .replacing(pattern: "\\\\{2}", with: "\\\\") 62 | } 63 | 64 | // Duration 65 | func buildProperty(of name: String) -> ICalDuration? { 66 | guard let prop = findProperty(name: name) else { 67 | return nil 68 | } 69 | 70 | return PropertyBuilder.buildDuration(value: prop.value) 71 | } 72 | 73 | // Array 74 | func buildProperty(of name: String) -> [String] { 75 | guard let prop = findProperty(name: name) else { 76 | return [] 77 | } 78 | 79 | return prop.value.components(separatedBy: ",") 80 | } 81 | 82 | // RRule 83 | func buildProperty(of name: String) -> ICalRRule? { 84 | guard let prop = findProperty(name: name) else { 85 | return nil 86 | } 87 | 88 | return PropertyBuilder.buildRRule(value: prop.value) 89 | } 90 | 91 | // URL 92 | func buildProperty(of name: String) -> URL? { 93 | guard let prop = findProperty(name: name) else { 94 | return nil 95 | } 96 | 97 | return URL(string: prop.value) 98 | } 99 | 100 | // Attachment 101 | func buildProperty(of name: String) -> [ICalAttachment]? { 102 | guard let properties = findProperties(name: name) else { 103 | return nil 104 | } 105 | 106 | return properties.compactMap { prop in 107 | PropertyBuilder.buildAttachment(propName: prop.name, value: prop.value) 108 | } 109 | } 110 | 111 | // DateTimes 112 | func buildProperty(of name: String) -> ICalDateTimes? { 113 | guard let prop = findProperty(name: name) else { 114 | return nil 115 | } 116 | 117 | return PropertyBuilder.buildDateTimes(propName: prop.name, value: prop.value) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/Component/ICalEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalEvent.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | /// Provides a grouping of component properties that 10 | /// describes an event. 11 | /// 12 | /// See https://tools.ietf.org/html/rfc5545#section-3.6.1 13 | public struct ICalEvent: VComponent { 14 | public let component = Constant.Component.event 15 | 16 | /// In the case of an iCalendar object that specifies a 17 | /// "METHOD" property, this property specifies the date and time that 18 | /// the instance of the iCalendar object was created. In the case of 19 | /// an iCalendar object that doesn't specify a "METHOD" property, this 20 | /// property specifies the date and time that the information 21 | /// associated with the calendar component was last revised in the 22 | /// calendar store. 23 | /// 24 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.7.2 25 | public var dtstamp: Date 26 | 27 | /// This property defines the persistent, globally unique 28 | /// identifier for the calendar component. 29 | /// 30 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.4.7 31 | public var uid: String 32 | 33 | /// This property defines the access classification for a 34 | /// calendar component. 35 | /// 36 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.3 37 | public var classification: String? 38 | 39 | /// This property specifies the date and time that the calendar 40 | /// information was created by the calendar user agent in the calendar 41 | /// store. 42 | /// 43 | /// Note: This is analogous to the creation date and time for a 44 | /// file in the file system. 45 | /// 46 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.7.1 47 | public var created: Date? 48 | 49 | /// This property provides a more complete description of the 50 | /// calendar component than that provided by the "SUMMARY" property. 51 | /// 52 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.5 53 | public var description: String? 54 | 55 | /// This property specifies when the calendar component begins. 56 | /// 57 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.2.4 58 | public var dtstart: ICalDateTime? 59 | 60 | /// This property specifies the date and time that the 61 | /// information associated with the calendar component was last 62 | /// revised in the calendar store. 63 | /// 64 | /// Note: This is analogous to the modification date and time for a 65 | /// file in the file system. 66 | /// 67 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.7.3 68 | public var lastModified: Date? 69 | 70 | /// This property defines the intended venue for the activity 71 | /// for the activity. 72 | /// 73 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.7 74 | public var location: String? 75 | 76 | /// This property defines the organizer for a calendar component. 77 | /// 78 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.4.3 79 | public var organizer: String? // TODO: Add more structure 80 | 81 | /// This property defines the relative priority for a calendar component. 82 | /// 83 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.9 84 | public var priority: Int? 85 | 86 | /// This property defines the revision sequence number of the 87 | /// calendar component within a sequence of revisions. 88 | /// 89 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.7.4 90 | public var seq: Int? 91 | 92 | /// This property defines the overall status or confirmation 93 | /// for the calendar component. 94 | /// 95 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.11 96 | public var status: String? 97 | 98 | /// This property defines a short summary or subject for the 99 | /// calendar component. 100 | /// 101 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.1.12 102 | public var summary: String? 103 | 104 | /// This property defines whether or not an event is 105 | /// transparent to busy time searches. 106 | /// 107 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.2.7 108 | public var transp: String? 109 | 110 | /// This property defines a Uniform Resource Locator (URL) 111 | /// associated with the iCalendar object. 112 | /// 113 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.4.6 114 | public var url: URL? 115 | 116 | // Mutually exclusive specifications of end date 117 | /// This property specifies the date and time that a calendar 118 | /// component ends. 119 | /// 120 | /// Must have the same 'ignoreTime'-value as tstart. 121 | /// Mutually exclusive to 'due'. 122 | /// 123 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.2.2 124 | public var dtend: ICalDateTime? { 125 | willSet { 126 | if newValue != nil { 127 | duration = nil 128 | } 129 | } 130 | } 131 | 132 | /// This property specifies a positive duration of time. 133 | /// 134 | /// Mutually exclusive to 'due'. 135 | /// 136 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.2.5 137 | public var duration: ICalDuration? { 138 | willSet { 139 | if newValue != nil { 140 | dtend = nil 141 | } 142 | } 143 | } 144 | 145 | /// This property is used in conjunction with the "UID" and 146 | /// "SEQUENCE" properties to identify a specific instance of a 147 | /// recurring "VEVENT", "VTODO", or "VJOURNAL" calendar component. 148 | /// The property value is the original value of the "DTSTART" property 149 | /// of the recurrence instance. 150 | /// 151 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.4.4 152 | public var recurrenceID: ICalDateTime? 153 | 154 | /// This property defines a rule or repeating pattern for 155 | /// recurring events, to-dos, journal entries, or time zone 156 | /// definitions. 157 | /// 158 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.5.3 159 | public var rrule: ICalRRule? 160 | 161 | /// This property defines the list of DATE-TIME values for 162 | /// recurring events, to-dos, journal entries, or time zone 163 | /// definitions. 164 | /// 165 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.5.2 166 | public var rdate: ICalDateTimes? 167 | 168 | /// This property defines the list of DATE-TIME exceptions for 169 | /// recurring events, to-dos, journal entries, or time zone 170 | /// definitions. 171 | /// 172 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.5.1 173 | public var exdate: ICalDateTimes? 174 | 175 | /// This property provides the capability to associate a 176 | /// document object with a calendar component. 177 | /// 178 | /// See https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.1 179 | public var attachments: [ICalAttachment]? 180 | 181 | /// key: custom property name 182 | /// value: String 183 | public var extendProperties: [String: String]? 184 | 185 | // TODO: Define properties that can be specified multiple times: 186 | // public var attendees 187 | // public var categories 188 | // public var comments 189 | // public var contacts 190 | // public var rstatus 191 | // public var related 192 | 193 | public var alarms: [ICalAlarm] 194 | 195 | public var timeZone: ICalTimeZone? 196 | 197 | public var isAllDay: Bool { 198 | dtstart?.isDateOnly == true 199 | } 200 | 201 | public var children: [VComponent] { 202 | guard let timeZone = self.timeZone else { return alarms } 203 | 204 | var children: [VComponent] = alarms 205 | children.insert(timeZone, at: 0) 206 | 207 | return children 208 | } 209 | 210 | public var properties: [VContentLine?] { 211 | [ 212 | .line(Constant.Prop.dtstamp, dtstamp), 213 | .line(Constant.Prop.uid, uid), 214 | .line(Constant.Prop.classification, classification), 215 | .line(Constant.Prop.created, created), 216 | .line(Constant.Prop.description, description), 217 | .line(Constant.Prop.dtstart, dtstart), 218 | .line(Constant.Prop.lastModified, lastModified), 219 | .line(Constant.Prop.location, location), 220 | .line(Constant.Prop.organizer, organizer), 221 | .line(Constant.Prop.priority, priority), 222 | .line(Constant.Prop.seq, seq), 223 | .line(Constant.Prop.status, status), 224 | .line(Constant.Prop.summary, summary), 225 | .line(Constant.Prop.transp, transp), 226 | .line(Constant.Prop.url, url), 227 | .line(Constant.Prop.dtend, dtend), 228 | .line(Constant.Prop.duration, duration), 229 | .line(Constant.Prop.recurrenceID, recurrenceID), 230 | .line(Constant.Prop.rrule, rrule), 231 | .line(Constant.Prop.rdate, rdate), 232 | .line(Constant.Prop.exdate, exdate), 233 | .lines(Constant.Prop.attach, attachments) 234 | ] + extendPropertiesLine 235 | } 236 | 237 | public var extendPropertiesLine: [VContentLine?] { 238 | extendProperties?.map { 239 | return .line($0.key, $0.value) 240 | } ?? [] 241 | } 242 | 243 | public init( 244 | dtstamp: Date = Date(), 245 | uid: String = UUID().uuidString, 246 | classification: String? = nil, 247 | created: Date? = Date(), 248 | description: String? = nil, 249 | dtstart: ICalDateTime? = nil, 250 | lastModified: Date? = Date(), 251 | location: String? = nil, 252 | organizer: String? = nil, 253 | priority: Int? = nil, 254 | seq: Int? = nil, 255 | status: String? = nil, 256 | summary: String? = nil, 257 | transp: String? = nil, 258 | url: URL? = nil, 259 | dtend: ICalDateTime? = nil, 260 | duration: ICalDuration? = nil, 261 | recurrenceID: ICalDateTime? = nil, 262 | rrule: ICalRRule? = nil, 263 | rdate: ICalDateTimes? = nil, 264 | exdate: ICalDateTimes? = nil, 265 | alarms: [ICalAlarm] = [], 266 | timeZone: ICalTimeZone? = nil, 267 | attachments: [ICalAttachment]? = nil, 268 | extendProperties: [String: String]? = nil 269 | ) { 270 | self.dtstamp = dtstamp 271 | self.uid = uid 272 | self.classification = classification 273 | self.created = created 274 | self.description = description 275 | self.dtstart = dtstart 276 | self.lastModified = lastModified 277 | self.location = location 278 | self.organizer = organizer 279 | self.priority = priority 280 | self.seq = seq 281 | self.status = status 282 | self.summary = summary 283 | self.transp = transp 284 | self.url = url 285 | self.recurrenceID = recurrenceID 286 | self.rrule = rrule 287 | self.rdate = rdate 288 | self.exdate = exdate 289 | self.dtend = dtend 290 | self.duration = duration 291 | self.alarms = alarms 292 | self.timeZone = timeZone 293 | self.attachments = attachments 294 | self.extendProperties = extendProperties 295 | 296 | assert(dtend == nil || duration == nil, "End date/time and duration must not be specified together!") 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /Sources/Component/ICalSubTimeZone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalSubTimeZone.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | /// Provide a grouping of component properties that defines a 10 | /// time zone. 11 | /// 12 | /// See https://tools.ietf.org/html/rfc5545#section-3.6.5 13 | public class ICalSubTimeZone: VComponent { 14 | public let component = Constant.Component.daylight 15 | 16 | /// This property specifies when the calendar component begins. 17 | /// 18 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.2.4 19 | public var dtstart: Date 20 | 21 | /// This property specifies the offset that is in use in this 22 | /// time zone observance. 23 | /// 24 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.3.4 25 | public var tzOffsetTo: String 26 | 27 | /// This property specifies the offset that is in use prior to 28 | /// this time zone observance. 29 | /// 30 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.3.3 31 | public var tzOffsetFrom: String 32 | 33 | /// This property defines a rule or repeating pattern for 34 | /// recurring events, to-dos, journal entries, or time zone 35 | /// definitions. 36 | /// 37 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.5.3 38 | public var rrule: ICalRRule? 39 | 40 | /// This property specifies the customary designation for a 41 | /// time zone description. 42 | /// 43 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.3.2 44 | public var tzName: String? 45 | 46 | public var properties: [VContentLine?] { 47 | [ 48 | .line(Constant.Prop.tzOffsetFrom, tzOffsetFrom), 49 | .line(Constant.Prop.rrule, rrule), 50 | .line(Constant.Prop.dtstart, dtstart), 51 | .line(Constant.Prop.tzName, tzName), 52 | .line(Constant.Prop.tzOffsetTo, tzOffsetTo) 53 | ] 54 | } 55 | 56 | public init( 57 | dtstart: Date, 58 | tzOffsetTo: String, 59 | tzOffsetFrom: String, 60 | rrule: ICalRRule? = nil, 61 | tzName: String? = nil 62 | ) { 63 | self.dtstart = dtstart 64 | self.tzOffsetTo = tzOffsetTo 65 | self.tzOffsetFrom = tzOffsetFrom 66 | self.rrule = rrule 67 | self.tzName = tzName 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Component/ICalTimeZone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalTimeZone.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | /// Provide a grouping of component properties that defines a 10 | /// time zone. 11 | /// 12 | /// See https://tools.ietf.org/html/rfc5545#section-3.6.5 13 | public struct ICalTimeZone: VComponent { 14 | public let component = Constant.Component.timeZone 15 | 16 | /// This property defines the time zone, that 17 | /// will be use in the event. 18 | /// 19 | /// See https://tools.ietf.org/html/rfc5545#section-3.6.5 20 | public var tzid: String 21 | 22 | /// This property defines the value of the object `StandardComponent` 23 | /// which is the standard time of the Time Zone. 24 | /// 25 | /// See https://tools.ietf.org/html/rfc5545#section-3.6.5 26 | public var standard: ICalSubTimeZone? 27 | 28 | /// This property defines the value of the object `DaylightComponent` 29 | /// which is the standard time of the Time Zone. 30 | /// 31 | /// See https://tools.ietf.org/html/rfc5545#section-3.6.5 32 | public var daylight: ICalSubTimeZone? 33 | 34 | public var properties: [VContentLine?] { 35 | [ 36 | .line(Constant.Prop.tzid, tzid), 37 | ] 38 | } 39 | 40 | public var children: [VComponent] { 41 | var subComponent = [VComponent]() 42 | 43 | if let standard = standard { 44 | subComponent.append(standard) 45 | } 46 | 47 | if let daylight = daylight { 48 | subComponent.append(daylight) 49 | } 50 | 51 | return subComponent 52 | } 53 | 54 | public init( 55 | tzid: String, 56 | standard: ICalSubTimeZone? = nil, 57 | daylight: ICalSubTimeZone? = nil 58 | ) { 59 | assert( 60 | standard != nil || daylight != nil , 61 | "One of 'standardc' or 'daylightc' MUST occur and each MAY occur more than once." 62 | ) 63 | 64 | self.tzid = tzid 65 | self.standard = standard 66 | self.daylight = daylight 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Component/ICalendar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalendar.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | /// A collection of calendaring and scheduling information. 10 | /// 11 | /// See https://tools.ietf.org/html/rfc5545#section-3.4 12 | public struct ICalendar: VComponent { 13 | public let component = Constant.Component.calendar 14 | 15 | /// The identifier corresponding to the highest version number 16 | /// or the minimum and maximum range of the iCalendar specification 17 | /// that is required in order to interpret the iCalendar object. 18 | /// 19 | /// See https://tools.ietf.org/html/rfc5545#section-3.7.4 20 | public let version = "2.0" 21 | 22 | /// The identifier for the product that created the iCalendar 23 | /// object. 24 | /// 25 | /// See https://tools.ietf.org/html/rfc5545#section-3.7.3 26 | public var prodid: ICalProductIdentifier 27 | 28 | /// The calendar scale for the calendar information specified 29 | /// in this iCalendar object. 30 | /// 31 | /// See https://tools.ietf.org/html/rfc5545#section-3.7.1 32 | public var calscale: String? 33 | 34 | /// The iCalendar object method associated with the calendar 35 | /// object. 36 | /// 37 | /// See https://tools.ietf.org/html/rfc5545#section-3.7.2 38 | public var method: String? 39 | 40 | public var events: [ICalEvent] 41 | public var timeZones: [ICalTimeZone] 42 | public var alarms: [ICalAlarm] 43 | 44 | public var children: [VComponent] { 45 | [events, timeZones, alarms] 46 | .compactMap { $0 as? [VComponent] } 47 | .flatMap { $0 } 48 | } 49 | 50 | public var properties: [VContentLine?] { 51 | [ 52 | .line(Constant.Prop.version, version), 53 | .line(Constant.Prop.prodid, prodid), 54 | .line(Constant.Prop.calscale, calscale), 55 | .line(Constant.Prop.method, method) 56 | ] 57 | } 58 | 59 | public init( 60 | prodid: ICalProductIdentifier = .init(), 61 | calscale: String? = nil, 62 | method: String? = nil, 63 | events: [ICalEvent] = [], 64 | timeZones: [ICalTimeZone] = [], 65 | alarms: [ICalAlarm] = [] 66 | ) { 67 | self.prodid = prodid 68 | self.calscale = calscale ?? "GREGORIAN" 69 | self.method = method 70 | self.events = events 71 | self.timeZones = timeZones 72 | self.alarms = alarms 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Constant/Constant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constant.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | public enum Constant { 10 | public enum Prop { 11 | public static let begin = "BEGIN" 12 | public static let end = "END" 13 | 14 | public static let dtstamp = "DTSTAMP" 15 | public static let uid = "UID" 16 | public static let classification = "CLASS" 17 | public static let created = "CREATED" 18 | public static let description = "DESCRIPTION" 19 | public static let dtstart = "DTSTART" 20 | public static let lastModified = "LAST-MODIFIED" 21 | public static let location = "LOCATION" 22 | public static let organizer = "ORGANIZER" 23 | public static let priority = "PRIORITY" 24 | public static let seq = "SEQ" 25 | public static let status = "STATUS" 26 | public static let summary = "SUMMARY" 27 | public static let transp = "TRANSP" 28 | public static let url = "URL" 29 | public static let dtend = "DTEND" 30 | public static let duration = "DURATION" 31 | public static let recurrenceID = "RECURRENCE-ID" 32 | public static let rrule = "RRULE" 33 | public static let rdate = "RDATE" 34 | public static let exdate = "EXDATE" 35 | 36 | public static let version = "VERSION" 37 | public static let prodid = "PRODID" 38 | public static let calscale = "CALSCALE" 39 | public static let method = "METHOD" 40 | 41 | public static let tzOffsetFrom = "TZOFFSETFROM" 42 | public static let tzName = "TZNAME" 43 | public static let tzOffsetTo = "TZOFFSETTO" 44 | public static let tzid = "TZID" 45 | 46 | public static let action = "ACTION" 47 | public static let trigger = "TRIGGER" 48 | public static let repetition = "REPEAT" 49 | public static let attach = "ATTACH" 50 | 51 | public static let frequency = "FREQ" 52 | public static let interval = "INTERVAL" 53 | public static let until = "UNTIL" 54 | public static let count = "COUNT" 55 | public static let bySecond = "BYSECOND" 56 | public static let byMinute = "BYMINUTE" 57 | public static let byHour = "BYHOUR" 58 | public static let byDay = "BYDAY" 59 | public static let byDayOfMonth = "BYMONTHDAY" 60 | public static let byDayOfYear = "BYYEARDAY" 61 | public static let byWeekOfYear = "BYWEEKNO" 62 | public static let byMonth = "BYMONTH" 63 | public static let bySetPos = "BYSETPOS" 64 | public static let startOfWorkweek = "WKST" 65 | } 66 | 67 | public enum Component { 68 | public static let calendar = "VCALENDAR" 69 | public static let event = "VEVENT" 70 | public static let alarm = "VALARM" 71 | public static let timeZone = "VTIMEZONE" 72 | public static let daylight = "DAYLIGHT" 73 | public static let standard = "STANDARD" 74 | } 75 | 76 | public enum Format { 77 | public static let dateOnly = "yyyyMMdd" 78 | public static let dt = "yyyyMMdd'T'HHmmss" 79 | public static let utc = "yyyyMMdd'T'HHmmss'Z'" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Extension/Bool+VPropertyEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bool+VPropertyEncodable.swift 3 | // 4 | // 5 | // 6 | 7 | extension Bool: VPropertyEncodable { 8 | public var vEncoded: String { 9 | self ? "TRUE" : "FALSE" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Extension/Date+VPropertyEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+VPropertyEncodable.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | extension Date: VPropertyEncodable { 10 | public var vEncoded: String { 11 | ICalDateTime(type: .dateTime, date: self, tzid: nil).vEncoded 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Extension/Int+VPropertyEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+VPropertyEncodable.swift 3 | // 4 | // 5 | // 6 | 7 | extension Int: VPropertyEncodable { 8 | public var vEncoded: String { 9 | String(self) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Extension/String+Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Utilities.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | extension String { 10 | func chunks(ofLength length: Int) -> [String] { 11 | assert(length > 0, "Can only chunk string into non-empty slices.") 12 | 13 | guard !isEmpty else { return [""] } 14 | 15 | var chunks = [String]() 16 | var currentIndex = startIndex 17 | var remaining = count 18 | 19 | while currentIndex < endIndex { 20 | let nextIndex = index(currentIndex, offsetBy: min(length, remaining)) 21 | chunks.append(String(self[currentIndex.. String { 30 | guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { 31 | return self 32 | } 33 | let range = NSRange(0.. ICalendar? { 16 | let elements = icsToElements(ics) 17 | 18 | guard let prodIDValue = findProperty(name: Constant.Prop.prodid, elements: elements)?.value else { 19 | return nil 20 | } 21 | 22 | let prodSegments = segmentsOfProdID(prodIDValue) 23 | 24 | guard !prodSegments.isEmpty else { return nil } 25 | 26 | let method = findProperty(name: Constant.Prop.method, elements: elements)?.value 27 | let calscale = findProperty(name: Constant.Prop.calscale, elements: elements)?.value 28 | 29 | let vEvents = findComponent(name: Constant.Component.event, elements: elements) 30 | let vAlarams = findComponent(name: Constant.Component.alarm, elements: elements) 31 | let vTimeZones = findComponent(name: Constant.Component.timeZone, elements: elements) 32 | 33 | let events = buildEvents(components: vEvents) 34 | let alarms = buildAlarms(components: vAlarams) 35 | let timeZones = buildTimeZones(components: vTimeZones) 36 | 37 | return ICalendar( 38 | prodid: .init(segments: prodSegments), 39 | calscale: method, 40 | method: calscale, 41 | events: events, 42 | timeZones: timeZones, 43 | alarms: alarms) 44 | } 45 | 46 | public func parseEvents(ics: String) -> [ICalEvent] { 47 | let elements = icsToElements(ics) 48 | 49 | let vEvents = findComponent(name: Constant.Component.event, elements: elements) 50 | 51 | return buildEvents(components: vEvents) 52 | } 53 | 54 | public func parseAlarms(ics: String) -> [ICalAlarm] { 55 | let elements = icsToElements(ics) 56 | let vAlarams = findComponent(name: Constant.Component.alarm, elements: elements) 57 | 58 | return buildAlarms(components: vAlarams) 59 | } 60 | 61 | public func parseTimeZones(ics: String) -> [ICalTimeZone] { 62 | let elements = icsToElements(ics) 63 | let vTimeZones = findComponent(name: Constant.Component.timeZone, elements: elements) 64 | 65 | return buildTimeZones(components: vTimeZones) 66 | } 67 | 68 | public func parseDuration(value: String) -> ICalDuration { 69 | return PropertyBuilder.buildDuration(value: value) 70 | } 71 | 72 | public func parseRRule(value: String) -> ICalRRule? { 73 | return PropertyBuilder.buildRRule(value: value) 74 | } 75 | 76 | // MARK: - Build component 77 | 78 | /// VEvent 79 | private func buildEvents(components: [ICalComponent]) -> [ICalEvent] { 80 | 81 | return components.map { component -> ICalEvent in 82 | var event = ICalEvent() 83 | 84 | let vAlarams = findComponent(name: Constant.Component.alarm, elements: component.children) 85 | event.alarms = buildAlarms(components: vAlarams) 86 | 87 | event.dtstamp = component.buildProperty(of: Constant.Prop.dtstamp)?.date ?? Date() 88 | event.uid = component.buildProperty(of: Constant.Prop.uid) ?? String() 89 | event.classification = component.buildProperty(of: Constant.Prop.classification) 90 | event.created = component.buildProperty(of: Constant.Prop.created)?.date ?? Date() 91 | event.description = component.buildProperty(of: Constant.Prop.description) 92 | event.dtstart = component.buildProperty(of: Constant.Prop.dtstart) 93 | event.lastModified = component.buildProperty(of: Constant.Prop.lastModified)?.date ?? Date() 94 | event.location = component.buildProperty(of: Constant.Prop.location) 95 | event.organizer = component.buildProperty(of: Constant.Prop.organizer) 96 | event.priority = component.buildProperty(of: Constant.Prop.priority) 97 | event.seq = component.buildProperty(of: Constant.Prop.seq) 98 | event.status = component.buildProperty(of: Constant.Prop.status) 99 | event.summary = component.buildProperty(of: Constant.Prop.summary) 100 | event.transp = component.buildProperty(of: Constant.Prop.transp) 101 | event.url = component.buildProperty(of: Constant.Prop.url) 102 | event.dtend = component.buildProperty(of: Constant.Prop.dtend) 103 | event.duration = component.buildProperty(of: Constant.Prop.duration) 104 | event.recurrenceID = component.buildProperty(of: Constant.Prop.recurrenceID) 105 | event.rrule = component.buildProperty(of: Constant.Prop.rrule) 106 | event.rdate = component.buildProperty(of: Constant.Prop.rdate) 107 | event.exdate = component.buildProperty(of: Constant.Prop.exdate) 108 | event.attachments = component.buildProperty(of: Constant.Prop.attach) 109 | 110 | event.extendProperties = component.findExtendProperties() 111 | 112 | return event 113 | } 114 | } 115 | 116 | /// VAlarm 117 | private func buildAlarms(components: [ICalComponent]) -> [ICalAlarm] { 118 | return components.compactMap { component -> ICalAlarm? in 119 | guard let action = component.findProperty(name: Constant.Prop.action)?.value, 120 | let triggerProp = component.findProperty(name: Constant.Prop.trigger) 121 | else { 122 | return nil 123 | } 124 | 125 | guard let trigger = PropertyBuilder.buildDateTime(propName: triggerProp.name, value: triggerProp.value) else { 126 | return nil 127 | } 128 | 129 | var alarm = ICalAlarm(action: action, trigger: trigger.date) 130 | alarm.description = component.buildProperty(of: Constant.Prop.description) 131 | alarm.summary = component.buildProperty(of: Constant.Prop.summary) 132 | alarm.duration = component.buildProperty(of: Constant.Prop.duration) 133 | alarm.repetition = component.buildProperty(of: Constant.Prop.repetition) 134 | alarm.attach = component.buildProperty(of: Constant.Prop.attach) 135 | 136 | return alarm 137 | } 138 | } 139 | 140 | /// VTimeZone 141 | private func buildTimeZones(components: [ICalComponent]) -> [ICalTimeZone] { 142 | return components.compactMap { component -> ICalTimeZone? in 143 | guard let tzid = component.findProperty(name: Constant.Prop.tzid)?.value else { 144 | return nil 145 | } 146 | 147 | let standardElement = findComponent( 148 | name: Constant.Component.standard, 149 | elements: component.properties 150 | ).first 151 | 152 | let daylightElement = findComponent( 153 | name: Constant.Component.daylight, 154 | elements: component.properties 155 | ).first 156 | 157 | let standard = buildSubTimeZone(component: standardElement) 158 | let daylight = buildSubTimeZone(component: daylightElement) 159 | 160 | // One of 'standardc' or 'daylightc' MUST occur and each MAY occur more than once. 161 | if standard == nil && daylight == nil { 162 | return nil 163 | } 164 | 165 | return ICalTimeZone(tzid: tzid, standard: standard, daylight: daylight) 166 | } 167 | } 168 | 169 | /// TimeZone sub component 170 | private func buildSubTimeZone(component: ICalComponent?) -> ICalSubTimeZone? { 171 | guard let component = component else { return nil } 172 | 173 | guard let dtStartValue = component.findProperty(name: Constant.Prop.dtstart)?.value, 174 | let tzOffsetTo = component.findProperty(name: Constant.Prop.tzOffsetTo)?.value, 175 | let tzOffsetFrom = component.findProperty(name: Constant.Prop.tzOffsetFrom)?.value 176 | else { 177 | return nil 178 | } 179 | 180 | guard let dtStart = PropertyBuilder.buildDateTime(propName: Constant.Prop.dtstart, value: dtStartValue)?.date else { 181 | return nil 182 | } 183 | 184 | let subTimeZone = ICalSubTimeZone( 185 | dtstart: dtStart, 186 | tzOffsetTo: tzOffsetTo, 187 | tzOffsetFrom: tzOffsetFrom) 188 | 189 | subTimeZone.rrule = component.buildProperty(of: Constant.Prop.rrule) 190 | subTimeZone.tzName = component.buildProperty(of: Constant.Prop.tzName) 191 | 192 | return subTimeZone 193 | } 194 | 195 | // MARK: - Supporting function 196 | 197 | private func icsToElements(_ ics: String) -> [(name: String, value: String)] { 198 | return ics 199 | .replacing(pattern: "(\r?\n)+[ \t]", with: "") 200 | .components(separatedBy: "\n") 201 | .map { $0.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true) } 202 | .filter { $0.count > 1 } 203 | .map { (String($0[0]), String($0[1])) } 204 | } 205 | 206 | private func findComponent( 207 | name: String, 208 | elements: [(name: String, value: String)] 209 | ) -> [ICalComponent] { 210 | var founds = [ICalComponent]() 211 | var currentComponent: [(String, String)]? 212 | var childComponent: [(String, String)]? 213 | 214 | for element in elements { 215 | if element.name == Constant.Prop.begin, element.value == name { 216 | if currentComponent == nil { 217 | currentComponent = [] 218 | } 219 | } 220 | 221 | if currentComponent != nil { 222 | if element.name == Constant.Prop.begin, element.value != name { 223 | childComponent = [] 224 | } 225 | 226 | if childComponent != nil { 227 | childComponent?.append(element) 228 | } else { 229 | currentComponent?.append(element) 230 | } 231 | } 232 | 233 | if element.name == Constant.Prop.end, element.value == name { 234 | if let currentComponent = currentComponent { 235 | let componentElement = ICalComponent( 236 | properties: currentComponent, 237 | children: childComponent ?? []) 238 | founds.append(componentElement) 239 | } 240 | currentComponent = nil 241 | childComponent = nil 242 | } 243 | } 244 | 245 | return founds 246 | } 247 | 248 | private func findProperty( 249 | name: String, 250 | elements: [(name: String, value: String)] 251 | ) -> (name: String, value: String)? { 252 | return elements 253 | .filter { $0.name.hasPrefix(name) } 254 | .first 255 | } 256 | 257 | private func segmentsOfProdID(_ value: String) -> [String] { 258 | return value 259 | .components(separatedBy: "//") 260 | .filter { !$0.isEmpty && $0 != "-" } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/Protocol/VComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VComponent.swift 3 | // 4 | // 5 | // 6 | 7 | /// A component enclosed by BEGIN: and END:. 8 | public protocol VComponent: VEncodable { 9 | /// The component's 'type' that is used in the BEGIN/END 10 | /// declaration. 11 | var component: String { get } 12 | 13 | /// The component's properties. 14 | var properties: [VContentLine?] { get } 15 | 16 | /// The component's children. 17 | var children: [VComponent] { get } 18 | } 19 | 20 | public extension VComponent { 21 | var properties: [VContentLine?] { [] } 22 | var children: [VComponent] { [] } 23 | 24 | var contentLines: [VContentLine?] { 25 | [.line(Constant.Prop.begin, component)] 26 | + properties 27 | + children.flatMap(\.contentLines) 28 | + [.line(Constant.Prop.end, component)] 29 | } 30 | 31 | var vEncoded: String { 32 | contentLines 33 | .compactMap { $0?.vEncoded } 34 | .joined() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Protocol/VEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VEncodable.swift 3 | // 4 | // 5 | // 6 | 7 | /// Represents something that can be encoded 8 | /// in a format like V or vCard. 9 | public protocol VEncodable { 10 | /// The encoded string in the format. 11 | var vEncoded: String { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Protocol/VPropertyEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VPropertyEncodable.swift 3 | // 4 | // 5 | // 6 | 7 | /// Represents something that can be encoded in 8 | /// a format like V, but may require 9 | /// additional parameters in the content line. 10 | public protocol VPropertyEncodable: VEncodable { 11 | /// The additional parameters. 12 | var parameters: [ICalParameter] { get } 13 | } 14 | 15 | public extension VPropertyEncodable { 16 | var parameters: [ICalParameter] { [] } 17 | 18 | func parameter(_ key: String) -> ICalParameter? { 19 | parameters.first(where: { $0.key == key }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Structure/ICalAttachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalAttachment.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | /// This property provides the capability to associate a 10 | /// document object with a calendar component. 11 | /// 12 | /// See https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.1 13 | public struct ICalAttachment: VPropertyEncodable { 14 | public var parameters: [ICalParameter] 15 | public var value: String 16 | 17 | public var vEncoded: String { 18 | value 19 | } 20 | 21 | public init(parameters: [ICalParameter], value: String) { 22 | self.parameters = parameters 23 | self.value = value 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Structure/ICalDateTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalDateTime.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | public enum DateValueType: Equatable { 10 | case date 11 | case dateTime 12 | case period 13 | } 14 | 15 | /// A date or date/time for use in calendar 16 | /// events, todos or free/busy-components. 17 | public struct ICalDateTime: VPropertyEncodable { 18 | public var type: DateValueType 19 | public var tzid: String? 20 | public var date: Date 21 | 22 | public var vEncoded: String { 23 | DateTimeUtil.dateFormatter(type: type, tzid: tzid).string(from: date) 24 | } 25 | 26 | public var parameters: [ICalParameter] { 27 | DateTimeUtil.params(type: type, tzid: tzid) 28 | } 29 | 30 | public var isDateOnly: Bool { 31 | type == .date 32 | } 33 | 34 | init(type: DateValueType, date: Date, tzid: String?) { 35 | self.type = type 36 | self.date = date 37 | self.tzid = tzid 38 | } 39 | 40 | public static func dateOnly(_ date: Date) -> ICalDateTime { 41 | ICalDateTime(type: .date, date: date, tzid: nil) 42 | } 43 | 44 | public static func dateTime(_ date: Date, tzid: String? = nil) -> ICalDateTime { 45 | ICalDateTime(type: .dateTime, date: date, tzid: tzid) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Structure/ICalDateTimes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalDateTimes.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | public struct ICalDateTimes: VPropertyEncodable { 10 | public var type: DateValueType 11 | public var tzid: String? 12 | public var dates: [Date] 13 | public var periods: [ICalPeriod]? 14 | 15 | public var vEncoded: String { 16 | if type == .period, let periods = periods { 17 | return periods.map { 18 | let formatter = DateTimeUtil.dateFormatter(type: type, tzid: tzid) 19 | return formatter.string(from: $0.startDate) + "/" + formatter.string(from: $0.endDate) 20 | } 21 | .joined(separator: ",") 22 | } else { 23 | return dates 24 | .map { DateTimeUtil.dateFormatter(type: type, tzid: tzid).string(from: $0) } 25 | .joined(separator: ",") 26 | } 27 | } 28 | 29 | public var parameters: [ICalParameter] { 30 | DateTimeUtil.params(type: type, tzid: tzid) 31 | } 32 | 33 | private init( 34 | type: DateValueType, 35 | dates: [Date], 36 | tzid: String?, 37 | periods: [ICalPeriod]? 38 | ) { 39 | self.type = type 40 | self.dates = dates 41 | self.tzid = tzid 42 | self.periods = periods 43 | } 44 | 45 | public static func dateOnly(_ dates: [Date]) -> ICalDateTimes { 46 | ICalDateTimes(type: .date, dates: dates, tzid: nil, periods: nil) 47 | } 48 | 49 | public static func dateTime(_ dates: [Date], tzid: String? = nil) -> ICalDateTimes { 50 | ICalDateTimes(type: .dateTime, dates: dates, tzid: tzid, periods: nil) 51 | } 52 | 53 | public static func period(_ periods: [ICalPeriod], tzid: String? = nil) -> ICalDateTimes { 54 | ICalDateTimes(type: .period, dates: [], tzid: tzid, periods: periods) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Structure/ICalDuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalDuration.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | fileprivate let second: Int = 1 10 | fileprivate let minute: Int = second * 60 11 | fileprivate let hour: Int = minute * 60 12 | fileprivate let day: Int = hour * 24 13 | fileprivate let week: Int = day * 7 14 | 15 | /// Specifies a positive duration of time. 16 | /// 17 | /// See https://tools.ietf.org/html/rfc5545#section-3.8.2.5 18 | public struct ICalDuration: VPropertyEncodable, AdditiveArithmetic { 19 | public static let zero: ICalDuration = ICalDuration(totalSeconds: 0) 20 | 21 | /// The total seconds of this day. 22 | public var totalSeconds: Int 23 | 24 | public var parts: (weeks: Int, days: Int, hours: Int, minutes: Int, seconds: Int) { 25 | if totalSeconds % week == 0 { 26 | let weeks = totalSeconds / week 27 | return (weeks: Int(weeks), days: 0, hours: 0, minutes: 0, seconds: 0) 28 | } 29 | 30 | let days = totalSeconds / day 31 | let rest1 = totalSeconds % day 32 | let hours = rest1 / hour 33 | let rest2 = rest1 % hour 34 | let minutes = rest2 / minute 35 | let rest3 = rest2 % minute 36 | let seconds = rest3 / second 37 | 38 | return (weeks: 0, days: Int(days), hours: Int(hours), minutes: Int(minutes), seconds: Int(seconds)) 39 | } 40 | 41 | public var vEncoded: String { 42 | var encodedDuration: String 43 | let (weeks, days, hours, minutes, seconds) = parts 44 | 45 | if totalSeconds % week == 0 { 46 | encodedDuration = "\(weeks)W" 47 | } else { 48 | encodedDuration = "\(days)DT\(hours)H\(minutes)M\(seconds)S" 49 | } 50 | 51 | return "\(totalSeconds >= 0 ? "" : "-")P\(encodedDuration)" 52 | } 53 | 54 | public init(totalSeconds: Int = 0) { 55 | self.totalSeconds = totalSeconds 56 | } 57 | 58 | public init(integerLiteral: Int) { 59 | self.init(totalSeconds: integerLiteral) 60 | } 61 | 62 | public init(weeks: Int, days: Int, hours: Int, minutes: Int, seconds: Int) { 63 | let weeksSec = weeks * week 64 | let daysSec = days * day 65 | let hoursSec = hours * hour 66 | let minutesSec = minutes * minute 67 | 68 | let totalSeconds = weeksSec + daysSec + hoursSec + minutesSec + seconds 69 | 70 | self.init(totalSeconds: totalSeconds) 71 | } 72 | 73 | public mutating func negate() { 74 | totalSeconds.negate() 75 | } 76 | 77 | public static prefix func -(operand: Self) -> Self { 78 | Self(totalSeconds: -operand.totalSeconds) 79 | } 80 | 81 | public static func +(lhs: Self, rhs: Self) -> Self { 82 | Self(totalSeconds: lhs.totalSeconds + rhs.totalSeconds) 83 | } 84 | 85 | public static func -(lhs: Self, rhs: Self) -> Self { 86 | Self(totalSeconds: lhs.totalSeconds - rhs.totalSeconds) 87 | } 88 | 89 | public static func weeks(_ weeks: Int) -> Self { 90 | Self(totalSeconds: weeks * week) 91 | } 92 | 93 | public static func days(_ days: Int) -> Self { 94 | Self(totalSeconds: days * day) 95 | } 96 | 97 | public static func hours(_ hours: Int) -> Self { 98 | Self(totalSeconds: hours * hour) 99 | } 100 | 101 | public static func minutes(_ minutes: Int) -> Self { 102 | Self(totalSeconds: minutes * minute) 103 | } 104 | 105 | public static func seconds(_ seconds: Int) -> Self { 106 | Self(totalSeconds: seconds * second) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Structure/ICalParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalParameter.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | public struct ICalParameter: Equatable { 10 | public let key: String 11 | public let values: [String] 12 | 13 | public init(key: String, values: [String]) { 14 | self.key = key 15 | self.values = values 16 | } 17 | 18 | public static func == (lhs: Self, rhs: Self) -> Bool { 19 | lhs.key == rhs.key 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Structure/ICalPeriod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalPeriod.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | public struct ICalPeriod: VPropertyEncodable { 10 | public var startDate: Date 11 | public var endDate: Date 12 | 13 | public var vEncoded: String { 14 | let formatter = DateTimeUtil.dateFormatter(type: .period, tzid: nil) 15 | return formatter.string(from: startDate) + "/" + formatter.string(from: endDate) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Structure/ICalProductIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalProductIdentifier.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | /// The identifier for the product that created the 10 | /// iCalendar object. 11 | /// 12 | /// See https://tools.ietf.org/html/rfc5545#section-3.7.3 13 | public struct ICalProductIdentifier: VPropertyEncodable { 14 | public let segments: [String] 15 | 16 | public var vEncoded: String { 17 | "-\(segments.map { "//\($0)" }.joined())" 18 | } 19 | 20 | //TODO add UUID 21 | public init(segments: [String] = ["calendar", "iCalSwift", "EN"]) { 22 | self.segments = segments 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Structure/ICalRRule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICalRRule.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | /// This value type is used to identify properties that contain 10 | /// a recurrence rule specification. 11 | /// 12 | /// See https://tools.ietf.org/html/rfc5545#section-3.3.10 13 | public struct ICalRRule: VPropertyEncodable { 14 | 15 | /// The frequency of the recurrence. 16 | public var frequency: Frequency 17 | 18 | /// At which interval the recurrence repeats (in terms of the frequency). 19 | /// E.g. 1 means every hour for an hourly rule, ... 20 | /// The default value is 1. 21 | public var interval: Int? 22 | 23 | /// The end date/time. Must have the same 'ignoreTime'-value as dtstart. 24 | public var until: ICalDateTime? { 25 | willSet { 26 | if newValue != nil { 27 | count = nil 28 | } 29 | } 30 | } 31 | 32 | /// The number of recurrences. 33 | public var count: Int? { 34 | willSet { 35 | if newValue != nil { 36 | until = nil 37 | } 38 | } 39 | } 40 | 41 | /// At which seconds of the minute it should occur. 42 | /// Must be between 0 and 60 (inclusive). 43 | public var bySecond: [Int]? { 44 | didSet { assert(bySecond?.allSatisfy { (0...60).contains($0) } ?? true, "by-second rules must be between 0 and 60 (inclusive): \(bySecond ?? [])") } 45 | } 46 | 47 | /// At which minutes of the hour it should occur. 48 | /// Must be between 0 and 60 (exclusive). 49 | public var byMinute: [Int]? { 50 | didSet { assert(byMinute?.allSatisfy { (0..<60).contains($0) } ?? true, "by-hour rules must be between 0 and 60 (exclusive): \(byMinute ?? [])") } 51 | } 52 | 53 | /// At which hours of the day it should occur. 54 | /// Must be between 0 and 24 (exclusive). 55 | public var byHour: [Int]? { 56 | didSet { assert(byHour?.allSatisfy { (0..<24).contains($0) } ?? true, "by-hour rules must be between 0 and 24 (exclusive): \(byHour ?? [])") } 57 | } 58 | 59 | /// At which days (of the week/year) it should occur. 60 | public var byDay: [Day]? 61 | 62 | /// At which days of the month it should occur. Specifies a COMMA-separated 63 | /// list of days of the month. Valid values are 1 to 31 or -31 to -1. 64 | public var byDayOfMonth: [Int]? { 65 | didSet { assert(byDayOfMonth?.allSatisfy { (1...31).contains(abs($0)) } ?? true, "by-set-pos rules must be between 1 and 31 or -31 and -1: \(byDayOfMonth ?? [])") } 66 | } 67 | 68 | /// At which days of the year it should occur. Specifies a list of days 69 | /// of the year. Valid values are 1 to 366 or -366 to -1. 70 | public var byDayOfYear: [Int]? { 71 | didSet { assert(byDayOfYear?.allSatisfy { (1...366).contains(abs($0)) } ?? true, "by-set-pos rules must be between 1 and 366 or -366 and -1: \(byDayOfYear ?? [])") } 72 | } 73 | 74 | /// At which weeks of the year it should occur. Specificies a list of 75 | /// ordinals specifying weeks of the year. Valid values are 1 to 53 or -53 to 76 | /// -1. 77 | public var byWeekOfYear: [Int]? { 78 | didSet { assert(byWeekOfYear?.allSatisfy { (1...53).contains(abs($0)) } ?? true, "by-set-pos rules must be between 1 and 53 or -53 and -1: \(byWeekOfYear ?? [])") } 79 | } 80 | 81 | /// At which months it should occur. 82 | /// Must be between 1 and 12 (inclusive). 83 | public var byMonth: [Int]? { 84 | didSet { assert(byMonth?.allSatisfy { (1...12).contains($0) } ?? true, "by-month-of-year rules must be between 1 and 12: \(byMonth ?? [])") } 85 | } 86 | 87 | /// Specifies a list of values that corresponds to the nth occurrence within 88 | /// the set of recurrence instances specified by the rule. By-set-pos 89 | /// operates on a set of recurrence instances in one interval of the 90 | /// recurrence rule. For example, in a weekly rule, the interval would be one 91 | /// week A set of recurrence instances starts at the beginning of the 92 | /// interval defined by the frequency rule part. Valid values are 1 to 366 or 93 | /// -366 to -1. It MUST only be used in conjunction with another by-xxx rule 94 | /// part. 95 | public var bySetPos: [Int]? { 96 | didSet { assert(bySetPos?.allSatisfy { (1...366).contains(abs($0)) } ?? true, "by-set-pos rules must be between 1 and 366 or -366 and -1: \(bySetPos ?? [])") } 97 | } 98 | 99 | /// The day on which the workweek starts. 100 | /// Monday by default. 101 | public var startOfWorkweek: DayOfWeek? 102 | 103 | private var properties: [(String, [VEncodable]?)] { 104 | [ 105 | (Constant.Prop.frequency, [frequency]), 106 | (Constant.Prop.interval, interval.map { [$0] }), 107 | (Constant.Prop.until, until.map { [$0] }), 108 | (Constant.Prop.count, count.map { [$0] }), 109 | (Constant.Prop.bySecond, bySecond), 110 | (Constant.Prop.byMinute, byMinute), 111 | (Constant.Prop.byHour, byHour), 112 | (Constant.Prop.byDay, byDay), 113 | (Constant.Prop.byDayOfMonth, byDayOfMonth), 114 | (Constant.Prop.byDayOfYear, byDayOfYear), 115 | (Constant.Prop.byWeekOfYear, byWeekOfYear), 116 | (Constant.Prop.byMonth, byMonth), 117 | (Constant.Prop.bySetPos, bySetPos), 118 | (Constant.Prop.startOfWorkweek, startOfWorkweek.map { [$0] }) 119 | ] 120 | } 121 | 122 | public var vEncoded: String { 123 | properties.compactMap { (key, values) in 124 | values.map { "\(key)=\($0.map(\.vEncoded).joined(separator: ","))" } 125 | }.joined(separator: ";") 126 | } 127 | 128 | public enum Frequency: String, VEncodable { 129 | case secondly = "SECONDLY" 130 | case minutely = "MINUTELY" 131 | case hourly = "HOURLY" 132 | case daily = "DAILY" 133 | case weekly = "WEEKLY" 134 | case monthly = "MONTHLY" 135 | case yearly = "YEARLY" 136 | 137 | public var vEncoded: String { rawValue } 138 | } 139 | 140 | public enum DayOfWeek: String, VEncodable { 141 | case monday = "MO" 142 | case tuesday = "TU" 143 | case wednesday = "WE" 144 | case thursday = "TH" 145 | case friday = "FR" 146 | case saturday = "SA" 147 | case sunday = "SU" 148 | 149 | public var vEncoded: String { rawValue } 150 | 151 | public var weekday: Int { 152 | switch self { 153 | case .monday: 154 | return 2 155 | case .tuesday: 156 | return 3 157 | case .wednesday: 158 | return 4 159 | case .thursday: 160 | return 5 161 | case .friday: 162 | return 6 163 | case .saturday: 164 | return 7 165 | case .sunday: 166 | return 1 167 | } 168 | } 169 | } 170 | 171 | public struct Day: VEncodable { 172 | /// The week. May be negative. 173 | public let week: Int? 174 | /// The day of the week. 175 | public let dayOfWeek: DayOfWeek 176 | 177 | public var vEncoded: String { "\(week.map(String.init) ?? "")\(dayOfWeek.vEncoded)" } 178 | 179 | public init(week: Int? = nil, dayOfWeek: DayOfWeek) { 180 | self.week = week 181 | self.dayOfWeek = dayOfWeek 182 | 183 | assert(week.map { (1...53).contains(abs($0)) } ?? true, "Week-of-year \(week.map(String.init) ?? "?") is not between 1 and 53 or -53 and -1 (each inclusive)") 184 | } 185 | 186 | public static func every(_ dayOfWeek: DayOfWeek) -> Self { 187 | Self(dayOfWeek: dayOfWeek) 188 | } 189 | 190 | public static func first(_ dayOfWeek: DayOfWeek) -> Self { 191 | Self(week: 1, dayOfWeek: dayOfWeek) 192 | } 193 | 194 | public static func last(_ dayOfWeek: DayOfWeek) -> Self { 195 | Self(week: -1, dayOfWeek: dayOfWeek) 196 | } 197 | 198 | public static func from(_ value: String) -> Self? { 199 | let index = value.index(value.startIndex, offsetBy: value.count - 2) 200 | 201 | let dayOfWeekStr = String(value[index...]) 202 | let weekStr = String(value[.. 1))" } 20 | .joined() 21 | 22 | let line = "\(key)\(paramsToString):\(value.vEncoded)" 23 | let chunks = line.chunks(ofLength: Self.maxLength) 24 | 25 | assert(!chunks.isEmpty) 26 | 27 | // From the RFC (section 3.1): 28 | // 29 | // Lines of text SHOULD NOT be longer than 75 octets, excluding the line 30 | // break. Long content lines SHOULD be split into a multiple line 31 | // representations using a line "folding" technique. That is, a long 32 | // line can be split between any two characters by inserting a CRLF 33 | // immediately followed by a single linear white-space character (i.e., 34 | // SPACE or HTAB). Any sequence of CRLF followed immediately by a 35 | // single linear white-space character is ignored (i.e., removed) when 36 | // processing the content type. 37 | 38 | return chunks 39 | .enumerated() 40 | .map { (index, chunk) in index > 0 ? " \(chunk)" : chunk } 41 | .map { "\($0)\r\n" } 42 | .joined() 43 | }.joined() 44 | 45 | return encoded 46 | } 47 | 48 | public init(key: String, values: [VPropertyEncodable]) { 49 | self.key = key 50 | self.values = values 51 | } 52 | 53 | // Multi line 54 | public static func lines(_ key: String, _ values: [VPropertyEncodable]?) -> VContentLine? { 55 | guard let values = values, !values.isEmpty else { 56 | return nil 57 | } 58 | 59 | return .init(key: key, values: values) 60 | } 61 | 62 | // Single line 63 | public static func line(_ key: String, _ value: VPropertyEncodable?) -> VContentLine? { 64 | guard let value = value else { 65 | return nil 66 | } 67 | 68 | return .init(key: key, values: [value]) 69 | } 70 | 71 | private func quote(_ str: String, if predicate: Bool) -> String { 72 | predicate ? "\"\(str)\"" : str 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Utils/DateTimeUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateTimeUtil.swift 3 | // 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | struct DateTimeUtil { 10 | static func dateFormatter(type: DateValueType, tzid: String?) -> DateFormatter { 11 | let formatter = DateFormatter() 12 | 13 | if let tzid = tzid { 14 | formatter.timeZone = .init(identifier: tzid) 15 | } else if .date == type { 16 | formatter.timeZone = .current 17 | } else { 18 | formatter.timeZone = .init(abbreviation: "UTC") 19 | } 20 | 21 | formatter.dateFormat = { 22 | switch type { 23 | case .date: 24 | return Constant.Format.dateOnly 25 | case .dateTime, .period: 26 | return tzid == nil ? Constant.Format.utc : Constant.Format.dt 27 | } 28 | }() 29 | 30 | return formatter 31 | } 32 | 33 | static func params(type: DateValueType, tzid: String?) -> [ICalParameter] { 34 | let valueParam: ICalParameter? = { 35 | switch type { 36 | case .date: 37 | return .init(key: "VALUE", values: ["DATE"]) 38 | case .dateTime: 39 | return nil 40 | case .period: 41 | return .init(key: "VALUE", values: ["PERIOD"]) 42 | } 43 | }() 44 | 45 | let tzidParam: ICalParameter? = { 46 | if let tzid = tzid { 47 | return .init(key: "TZID", values: [tzid]) 48 | } else { 49 | return nil 50 | } 51 | }() 52 | 53 | return [valueParam, tzidParam].compactMap { $0 } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/ICalSwiftTests/ICalSwiftTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ICalSwift 3 | 4 | final class ICalSwiftTests: XCTestCase { 5 | 6 | func testICalEventToVEncoded() throws { 7 | let event = ICalEvent( 8 | dtstamp: Date(), 9 | uid: "example@gmail.com", 10 | classification: nil, 11 | created: Date(), 12 | description: "example", 13 | dtstart: .init(date: Date()), 14 | lastModified: Date(), 15 | location: "1", 16 | organizer: nil, 17 | priority: 1, 18 | seq: nil, 19 | status: "CONFIRMED", 20 | summary: "Spinning", 21 | transp: "SPAQUE", 22 | url: nil, 23 | dtend: nil, 24 | duration: nil, 25 | recurrenceID: Date(), 26 | rrule: .init( 27 | frequency: .daily, 28 | interval: 30, 29 | until: nil, 30 | count: 3, 31 | bySecond: nil, 32 | byMinute: [10, 30], 33 | byHour: nil, 34 | byDay: [.first(.friday)], 35 | byDayOfMonth: nil, 36 | byDayOfYear: nil, 37 | byWeekOfYear: nil, 38 | byMonth: nil, 39 | bySetPos: nil, 40 | startOfWorkweek: .sunday), 41 | rdates: [Date(), Date(), Date()], 42 | exrule: nil, 43 | exdates: [Date(), Date()], 44 | alarms: [.audioProp(trigger: Date(), duration: .init(totalSeconds: 300), repetition: nil, attach: nil)], 45 | timeZone: nil, 46 | extendProperties: ["X-MAILPLUG-PROPERTY": "TEST"]) 47 | 48 | let vEncoded = event.vEncoded 49 | 50 | print(vEncoded) 51 | 52 | XCTAssertFalse(vEncoded.isEmpty) 53 | } 54 | } 55 | --------------------------------------------------------------------------------