├── .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 |
--------------------------------------------------------------------------------