Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/iCalendarParser/Constant/Constant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ extension Constant {
static let cname = "CN"

static let attachment: String = "ATTACH"
static let categories: String = "CATEGORIES"
static let created: String = "CREATED"
static let classification: String = "CLASS"
static let description: String = "DESCRIPTION"
Expand Down
4 changes: 3 additions & 1 deletion Sources/iCalendarParser/Models/ICEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public struct ICEvent: ICComponentable {
// https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.1))
// var attachment: [ICAttachment]?

// var categories: [String]?
public var categories: [String]?

/// Defines the access classification for a calendar component.
///
Expand Down Expand Up @@ -195,6 +195,7 @@ public struct ICEvent: ICComponentable {

public init(
attendees: [ICAttendee]? = nil,
categories: [String]? = nil,
classification: String? = nil,
description: String? = nil,
dtCreated: Date? = nil,
Expand All @@ -215,6 +216,7 @@ public struct ICEvent: ICComponentable {
url: URL? = nil
) {
self.attendees = attendees
self.categories = categories
self.classification = classification
self.description = description
self.dtCreated = dtCreated
Expand Down
22 changes: 15 additions & 7 deletions Sources/iCalendarParser/Parser/ICParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,29 @@ public struct ICParser {
)
}

func getProperties(
from ics: String
) -> [ICProperty] {
return ics
.replacingOccurrences(of: "\r\n ", with: "")
.components(separatedBy: "\r\n")
func getProperties(from ics: String) -> [ICProperty] {
// Normalize line endings to \n
let normalizedICS = ics
.replacingOccurrences(of: "\r\n", with: "\n")
.replacingOccurrences(of: "\r", with: "\n")
// Replace line folding sequences
let unfoldedICS = normalizedICS.replacingOccurrences(of: "\n ", with: "")

// Split by \n
let properties: [ICProperty] = unfoldedICS
.components(separatedBy: "\n")
.map { $0.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true) }
.filter { $0.count > 1 }
.map { (String($0[0]), String($0[1])) }

return properties
}

private func getProperty(
name: String,
from elements: [ICProperty]
) -> ICProperty? {
return elements
elements
.filter { $0.name.hasPrefix(name) }
.first
}
Expand Down Expand Up @@ -144,6 +151,7 @@ public struct ICParser {
var event = ICEvent()

event.attendees = component.buildAttendees(of: Constant.Property.attendee)
event.categories = component.buildProperty(of: Constant.Property.categories)?.components(separatedBy: ",")
event.classification = component.buildProperty(of: Constant.Property.classification)
event.description = component.buildProperty(of: Constant.Property.description)
event.dtCreated = component.buildProperty(of: Constant.Property.created)?.date
Expand Down
36 changes: 36 additions & 0 deletions Tests/iCalendarParserTests/Extensions/XCTestCase+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// XCTestCase+Extension.swift
//
//
// Created by Adrian Bolinger on 7/28/24.
//

import XCTest

extension XCTestCase {
/// Reads the contents of a file located in the test directory's "Resources" folder.
///
/// - Parameters:
/// - filename: The name of the file (without extension).
/// - ext: The extension of the file.
/// - Returns: The contents of the file as a String.
/// - Throws: An error if the file cannot be read.
func getContents(of filename: String, ext: String, file: StaticString = #filePath) throws -> String {
// Get the directory of the current file
let currentFilePath = String(describing: file)
let currentDirectoryPath = URL(fileURLWithPath: currentFilePath).deletingLastPathComponent()

// Construct the full file path
let fileURL = currentDirectoryPath.appendingPathComponent("Resources/\(filename).\(ext)")

// Ensure the file exists
guard FileManager.default.fileExists(atPath: fileURL.path) else {
throw NSError(domain: "FileNotFoundError", code: 1, userInfo: [NSLocalizedDescriptionKey: "File \(filename).\(ext) not found at path \(fileURL.path)."])
}

// Read the contents of the file
let fileContents = try String(contentsOf: fileURL, encoding: .utf8)

return fileContents
}
}
50 changes: 50 additions & 0 deletions Tests/iCalendarParserTests/ICParserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// ICParserTests.swift
//
//
// Created by Adrian Bolinger on 7/28/24.
//

import XCTest

@testable import iCalendarParser

final class ICParserTests: XCTestCase {

var sut: ICParser!

override func setUpWithError() throws {
try super.setUpWithError()
sut = ICParser()
}

override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}

func testICParser() throws {
let iCalString = try getContents(of: "uf-full-calendar", ext: "txt")
let calendar = sut.calendar(from: iCalString)
XCTAssertEqual(calendar?.events.count, 295)
}

func testICParserCategories() throws {
let iCalString = try getContents(of: "ufl-all-events", ext: "txt")
guard let calendar = sut.calendar(from: iCalString) else {
XCTFail("could not create calendar")
return
}

let uniqueCategories = calendar.events
.compactMap { $0.categories }
.flatMap { $0 }
.reduce(into: [String]()) { result, category in
if !result.contains(category) {
result.append(category)
}
}

XCTAssertEqual(uniqueCategories.count, 19)
}
}
Loading