diff --git a/Modules/Parser/.swiftpm/xcode/xcshareddata/xcschemes/Parser-Package.xcscheme b/Modules/Parser/.swiftpm/xcode/xcshareddata/xcschemes/Parser-Package.xcscheme index e1ee0e250..5ddd095ac 100644 --- a/Modules/Parser/.swiftpm/xcode/xcshareddata/xcschemes/Parser-Package.xcscheme +++ b/Modules/Parser/.swiftpm/xcode/xcshareddata/xcschemes/Parser-Package.xcscheme @@ -49,6 +49,20 @@ ReferencedContainer = "container:"> + + + + + + + + + + // MARK: - Public API + + /// Parse W3C and pubDate dates — used for feed parsing. + /// This is a fast alternative to system APIs + /// for parsing dates. + public static func date(data: Data) -> Date? { + + let numberOfBytes = data.count + + // Make sure it’s in reasonable range for a date string. + if numberOfBytes < 6 || numberOfBytes > 150 { + return nil + } + + return data.withUnsafeBytes { bytes in + let buffer = bytes.bindMemory(to: UInt8.self) + + if dateIsW3CDate(buffer, numberOfBytes) { + return parseW3CDate(buffer, numberOfBytes) + } + else if dateIsPubDate(buffer, numberOfBytes) { + return parsePubDate(buffer, numberOfBytes) + } + + // Fallback, in case our detection fails. + return parseW3CDate(buffer, numberOfBytes) + } + } +} + +// MARK: - Private + +private extension DateParser { + + struct DateCharacter { + + static let space = Character(" ").asciiValue + static let `return` = Character("\r").asciiValue + static let newline = Character("\n").asciiValue + static let tab = Character("\t").asciiValue + static let hyphen = Character("-").asciiValue + static let comma = Character(",").asciiValue + static let dot = Character(".").asciiValue + static let colon = Character(":").asciiValue + static let plus = Character("+").asciiValue + static let minus = Character("-").asciiValue + static let Z = Character("Z").asciiValue + static let z = Character("z").asciiValue + static let F = Character("F").asciiValue + static let f = Character("f").asciiValue + static let S = Character("S").asciiValue + static let s = Character("s").asciiValue + static let O = Character("O").asciiValue + static let o = Character("o").asciiValue + static let N = Character("N").asciiValue + static let n = Character("n").asciiValue + static let D = Character("D").asciiValue + static let d = Character("d").asciiValue + } + + enum Month: Int { + + January = 1, + February, + March, + April, + May, + June, + July, + August, + September, + October, + November, + December + } + + // MARK: - Standard Formats + + static func dateIsW3CDate(_ bytes: DateBuffer, numberOfBytes: Int) -> Bool { + + // Something like 2010-11-17T08:40:07-05:00 + // But might be missing T character in the middle. + // Looks for four digits in a row followed by a -. + + for i in 0.. Bool { + + for ch in bytes { + if ch == DateCharacter.space || ch == DateCharacter.comma { + return true + } + } + + return false + } + + static func parseW3CDate(_ bytes: DateBuffer, numberOfBytes: Int) -> Date { + + /*@"yyyy'-'MM'-'dd'T'HH':'mm':'ss" + @"yyyy-MM-dd'T'HH:mm:sszzz" + @"yyyy-MM-dd'T'HH:mm:ss'.'SSSzzz" + etc.*/ + + var finalIndex = 0 + + let year = nextNumericValue(bytes, numberOfBytes, 0, 4, &finalIndex) + let month = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex) + let day = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex) + let hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex) + let minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex) + let second = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex) + + let currentIndex = finalIndex + 1 + + let milliseconds = { + var ms = 0 + let hasMilliseconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == DateCharacter.dot) + if hasMilliseconds { + ms = nextNumericValue(bytes, numberOfBytes, currentIndex, 3, &finalIndex) + currentIndex = finalIndex + 1 + } + return ms + }() + + let timeZoneOffset = parsedtimeZoneOffset(bytes, numberOfBytes, currentIndex) + + return dateWithYearMonthDayHourMinuteSecondAndtimeZoneOffset(year, month, day, hour, minute, second, milliseconds, timeZoneOffset) + } + + static func parsePubDate(_ bytes: DateBuffer, numberOfBytes: Int) -> Date { + + var finalIndex = 0 + + let day = nextNumericValue(bytes, numberOfBytes, 0, 2, &finalIndex) ?? 1 + let month = nextMonthValue(bytes, numberOfBytes, finalIndex + 1, &finalIndex) + let year = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 4, &finalIndex) + let hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex) ?? 0 + let minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex) ?? 0 + + var currentIndex = finalIndex + 1 + + let second = { + var s = 0 + let hasSeconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == DateCharacter.colon) + if hasSeconds { + s = nextNumericValue(bytes, numberOfBytes, currentIndex, 2, &finalIndex) + } + return s + }() + + currentIndex = finalIndex + 1 + + let timeZoneOffset = { + var offset = 0 + let hasTimeZone = (currentIndex < numberOfBytes) && (bytes[currentIndex] == DateCharacter.space) + if hasTimeZone { + offset = parsedtimeZoneOffset(bytes, numberOfBytes, currentIndex) + } + return offset + }() + + return dateWithYearMonthDayHourMinuteSecondAndtimeZoneOffset(year, month, day, hour, minute, second, 0, timeZoneOffset) + } + + // MARK: - Date Creation + + static func dateWithYearMonthDayHourMinuteSecondAndtimeZoneOffset(_ year: Int, _ month: Int, _ day: Int, _ hour: Int, _ minute: Int, _ second: Int, _ milliseconds: Int, _ timeZoneOffset: Int) { + + var timeInfo = tm() + timeInfo.tm_sec = CInt(second) + timeInfo.tm_min = CInt(minute) + timeInfo.tm_hour = CInt(hour) + timeInfo.tm_mday = CInt(day) + timeInfo.tm_mon = CInt(month - 1) //It's 1-based coming in + timeInfo.tm_year = CInt(year - 1900) //see time.h -- it's years since 1900 + timeInfo.tm_wday = -1 + timeInfo.tm_yday = -1 + timeInfo.tm_isdst = -1 + timeInfo.tm_gmtoff = timeZoneOffset; + timeInfo.tm_zone = nil; + + var rawTime = timegm(&timeInfo) + if rawTime == time_t(UInt.max) { + + // NSCalendar is super-amazingly slow (which is partly why this parser exists), + // so this is used only when the date is far enough in the future + // (19 January 2038 03:14:08Z on 32-bit systems) that timegm fails. + // Hopefully by the time we consistently need dates that far in the future + // the performance of NSCalendar won’t be an issue. + + var dateComponents = DateComponents() + + dateComponents.timeZone = TimeZone(forSecondsFromGMT: timeZoneOffset) + dateComponents.year = year + dateComponents.month = month + dateComponents.day = day + dateComponents.hour = hour + dateComponents.minute = minute + dateComponents.second = second + (milliseconds / 1000) + + return Calendar.autoupdatingCurrent.date(from: dateComponents) + } + + if milliseconds > 0 { + rawTime += Float(milliseconds) / 1000.0 + } + + return Date(timeIntervalSince1970: rawTime) + } + + // MARK: - Time Zones and Offsets + + static let kGMT = "GMT".utf8CString + static let kUTC = "UTC".utf8CString + + static func parsedTimeZoneOffset(_ bytes: DateBuffer, _ numberOfBytes: Int, _ startingIndex: Int) -> Int { + + var timeZoneCharacters: [CChar] = [0, 0, 0, 0, 0, 0] // nil-terminated last character + var numberOfCharactersFound = 0 + var hasAtLeastOneAlphaCharacter = false + + for i in startingIndex..= 5 { + break + } + } + + if numberOfCharactersFound < 1 || timeZoneCharacters[0] == DateCharacter.Z || timeZoneCharacters[0] == DateCharacter.z { + return 0 + } + if strcasestr(timeZoneCharacters, kGMT) != nil || strcasestr(timeZoneCharacters, kUTC) != nil { + return 0 + } + + if hasAtLeastOneAlphaCharacter { + return offsetInSecondsForTimeZoneAbbreviation(timeZoneCharacters) + } + return offsetInSecondsForOffsetCharacters(timeZoneCharacters) + } + + static func offsetInSecondsForOffsetCharacters(_ timeZoneCharacters: DateBuffer) { + + let isPlus = timeZoneCharacters[0] == DateCharacter.plus + + var finalIndex = 0 + let numberOfCharacters = strlen(timeZoneCharacters) + let hours = nextNumericValue(timeZoneCharacters, numberOfCharacters, 0, 2, &finalIndex) ?? 0 + let minutes = nextNumericValue(timeZoneCharacters, numberOfCharacters, finalIndex + 1, 2, &finalIndex) ?? 0 + + if hours == 0 && minutes == 0 { + return 0 + } + + var seconds = (hours * 60 * 60) + (minutes * 60) + if !isPlus { + seconds = 0 - seconds + } + + return seconds + } + + /// Returns offset in seconds. + static func timeZoneOffset(_ hours: Int, _ minutes: Int) -> Int { + + if hours < 0 { + return (hours * 60 * 60) - (minutes * 60) + } + return (hours * 60 * 60) + (minutes * 60) + } + + // See http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations for list + private let timeZoneTable: [String: Int] = [ + + "GMT": timeZoneOffset(0, 0), + "PDT": timeZoneOffset(-7, 0), + "PST": timeZoneOffset(-8, 0), + "EST": timeZoneOffset(-5, 0), + "EDT": timeZoneOffset(-4, 0), + "MDT": timeZoneOffset(-6, 0), + "MST": timeZoneOffset(-7, 0), + "CST": timeZoneOffset(-6, 0), + "CDT": timeZoneOffset(-5, 0), + "ACT": timeZoneOffset(-8, 0), + "AFT": timeZoneOffset(4, 30), + "AMT": timeZoneOffset(4, 0), + "ART": timeZoneOffset(-3, 0), + "AST": timeZoneOffset(3, 0), + "AZT": timeZoneOffset(4, 0), + "BIT": timeZoneOffset(-12, 0), + "BDT": timeZoneOffset(8, 0), + "ACST": timeZoneOffset(9, 30), + "AEST": timeZoneOffset(10, 0), + "AKST": timeZoneOffset(-9, 0), + "AMST": timeZoneOffset(5, 0), + "AWST": timeZoneOffset(8, 0), + "AZOST": timeZoneOffset(-1, 0), + "BIOT": timeZoneOffset(6, 0), + "BRT": timeZoneOffset(-3, 0), + "BST": timeZoneOffset(6, 0), + "BTT": timeZoneOffset(6, 0), + "CAT": timeZoneOffset(2, 0), + "CCT": timeZoneOffset(6, 30), + "CET": timeZoneOffset(1, 0), + "CEST": timeZoneOffset(2, 0), + "CHAST": timeZoneOffset(12, 45), + "ChST": timeZoneOffset(10, 0), + "CIST": timeZoneOffset(-8, 0), + "CKT": timeZoneOffset(-10, 0), + "CLT": timeZoneOffset(-4, 0), + "CLST": timeZoneOffset(-3, 0), + "COT": timeZoneOffset(-5, 0), + "COST": timeZoneOffset(-4, 0), + "CVT": timeZoneOffset(-1, 0), + "CXT": timeZoneOffset(7, 0), + "EAST": timeZoneOffset(-6, 0), + "EAT": timeZoneOffset(3, 0), + "ECT": timeZoneOffset(-4, 0), + "EEST": timeZoneOffset(3, 0), + "EET": timeZoneOffset(2, 0), + "FJT": timeZoneOffset(12, 0), + "FKST": timeZoneOffset(-4, 0), + "GALT": timeZoneOffset(-6, 0), + "GET": timeZoneOffset(4, 0), + "GFT": timeZoneOffset(-3, 0), + "GILT": timeZoneOffset(7, 0), + "GIT": timeZoneOffset(-9, 0), + "GST": timeZoneOffset(-2, 0), + "GYT": timeZoneOffset(-4, 0), + "HAST": timeZoneOffset(-10, 0), + "HKT": timeZoneOffset(8, 0), + "HMT": timeZoneOffset(5, 0), + "IRKT": timeZoneOffset(8, 0), + "IRST": timeZoneOffset(3, 30), + "IST": timeZoneOffset(2, 0), + "JST": timeZoneOffset(9, 0), + "KRAT": timeZoneOffset(7, 0), + "KST": timeZoneOffset(9, 0), + "LHST": timeZoneOffset(10, 30), + "LINT": timeZoneOffset(14, 0), + "MAGT": timeZoneOffset(11, 0), + "MIT": timeZoneOffset(-9, 30), + "MSK": timeZoneOffset(3, 0), + "MUT": timeZoneOffset(4, 0), + "NDT": timeZoneOffset(-2, 30), + "NFT": timeZoneOffset(11, 30), + "NPT": timeZoneOffset(5, 45), + "NT": timeZoneOffset(-3, 30), + "OMST": timeZoneOffset(6, 0), + "PETT": timeZoneOffset(12, 0), + "PHOT": timeZoneOffset(13, 0), + "PKT": timeZoneOffset(5, 0), + "RET": timeZoneOffset(4, 0), + "SAMT": timeZoneOffset(4, 0), + "SAST": timeZoneOffset(2, 0), + "SBT": timeZoneOffset(11, 0), + "SCT": timeZoneOffset(4, 0), + "SLT": timeZoneOffset(5, 30), + "SST": timeZoneOffset(8, 0), + "TAHT": timeZoneOffset(-10, 0), + "THA": timeZoneOffset(7, 0), + "UYT": timeZoneOffset(-3, 0), + "UYST": timeZoneOffset(-2, 0), + "VET": timeZoneOffset(-4, 30), + "VLAT": timeZoneOffset(10, 0), + "WAT": timeZoneOffset(1, 0), + "WET": timeZoneOffset(0, 0), + "WEST": timeZoneOffset(1, 0), + "YAKT": timeZoneOffset(9, 0), + "YEKT": timeZoneOffset(5, 0) + ] + + static func offsetInSecondsForTimeZoneAbbreviation(_ abbreviation: DateBuffer) -> Int? { + + let name = String(cString: abbreviation) + return timeZoneTable[name] + } + + // MARK: - Parser + + static func nextMonthValue(_ buffer: DateBuffer, _ numberOfBytes: Int, _ startingIndex: Int, _ finalIndex: inout Int) -> DateParser.Month? { + + // Lots of short-circuits here. Not strict. + + var numberOfAlphaCharactersFound = 0 + var monthCharacters: [CChar] = [0, 0, 0] + + for i in startingIndex.. 0 { + break + } + } + + numberOfAlphaCharactersFound +=1 + if numberOfAlphaCharactersFound == 1 { + if ch == DateCharacter.F || ch == DateCharacter.f { + return February + } + if ch == DateCharacter.S || ch == DateCharacter.s { + return September + } + if ch == DateCharacter.O || ch == DateCharacter.o { + return October + } + if ch == DateCharacter.N || ch == DateCharacter.n { + return November + } + if ch == DateCharacter.D || ch == DateCharacter.d { + return December + } + } + + monthCharacters[numberOfAlphaCharactersFound - 1] = character + if numberOfAlphaCharactersFound >=3 + break + } + + if numberOfAlphaCharactersFound < 2 { + return nil + } + + if monthCharacters[0] == DateCharater.J || monthCharacters[0] == DateCharacter.j { // Jan, Jun, Jul + if monthCharacters[1] == DateCharacter.A || monthCharacters[1] == DateCharacter.a { + return Month.January + } + if monthCharacters[1] = DateCharacter.U || monthCharacters[1] == DateCharacter.u { + if monthCharacters[2] == DateCharacter.N || monthCharacters[2] == DateCharacter.n { + return June + } + return July + } + return January + } + + if monthCharacters[0] == DateCharacter.M || monthCharacters[0] == DateCharacter.m { // March, May + if monthCharacters[2] == DateCharacter.Y || monthCharacters[2] == DateCharacter.y { + return May + } + return March + } + + if monthCharacters[0] == DateCharacter.A || monthCharacters[0] == DateCharacter.a { // April, August + if monthCharacters[1] == DateCharacter.U || monthCharacters[1] == DateCharacter.u { + return August + } + return April + } + + return January // Should never get here (but possibly do) + } + + static func nextNumericValue(_ bytes: DateBuffer, numberOfBytes: Int, startingIndex: Int, maximumNumberOfDigits: Int, finalIndex: inout Int) -> Int? { + + // Maximum for the maximum is 4 (for time zone offsets and years) + assert(maximumNumberOfDigits > 0 && maximumNumberOfDigits <= 4) + + var numberOfDigitsFound = 0 + var digits = [0, 0, 0, 0] + + for i in startingIndex.. Int? { +// +// // Months are 1-based -- January is 1, Dec is 12. +// // Lots of short-circuits here. Not strict. GIGO +// +// var i = startingIndex +// var numberOfBytes = bytes.count +// var numberOfAlphaCharactersFound = 0 +// var monthCharacters = [Character]() +// +// while index < bytes.count { +// +// +// } +// +// +// var index = startingIndex +// var numberOfAlphaCharactersFound = 0 +// var monthCharacters: [Character] = [] +// +// while index < bytes.count { +// let character = bytes[bytes.index(bytes.startIndex, offsetBy: index)] +// +// if !character.isLetter, numberOfAlphaCharactersFound < 1 { +// index += 1 +// continue +// } +// if !character.isLetter, numberOfAlphaCharactersFound > 0 { +// break +// } +// +// numberOfAlphaCharactersFound += 1 +// if numberOfAlphaCharactersFound == 1 { +// switch character.lowercased() { +// case "f": return (.February.rawValue, index) +// case "s": return (.September.rawValue, index) +// case "o": return (.October.rawValue, index) +// case "n": return (.November.rawValue, index) +// case "d": return (.December.rawValue, index) +// default: break +// } +// } +// +// monthCharacters.append(character) +// if numberOfAlphaCharactersFound >= 3 { +// break +// } +// index += 1 +// } +// +// if numberOfAlphaCharactersFound < 2 { +// return (nil, index) +// } +// +// if monthCharacters[0].lowercased() == "j" { +// if monthCharacters[1].lowercased() == "a" { +// return (.January.rawValue, index) +// } +// if monthCharacters[1].lowercased() == "u" { +// if monthCharacters.count > 2 && monthCharacters[2].lowercased() == "n" { +// return (.June.rawValue, index) +// } +// return (.July.rawValue, index) +// } +// return (.January.rawValue, index) +// } +// +// if monthCharacters[0].lowercased() == "m" { +// if monthCharacters.count > 2 && monthCharacters[2].lowercased() == "y" { +// return (.May.rawValue, index) +// } +// return (.March.rawValue, index) +// } +// +// if monthCharacters[0].lowercased() == "a" { +// if monthCharacters[1].lowercased() == "u" { +// return (.August.rawValue, index) +// } +// return (.April.rawValue, index) +// } +// +// return (.January.rawValue, index) +//} +// +//func nextNumericValue(bytes: String, startingIndex: Int, maximumNumberOfDigits: Int) -> (Int?, Int) { +// let digits = bytes.dropFirst(startingIndex).prefix(maximumNumberOfDigits) +// guard let value = Int(digits) else { +// return (nil, startingIndex) +// } +// return (value, startingIndex + digits.count) +//} +// +//func hasAtLeastOneAlphaCharacter(_ s: String) -> Bool { +// return s.contains { $0.isLetter } +//} +// +//func offsetInSeconds(forTimeZoneAbbreviation abbreviation: String) -> Int { +// for zone in timeZoneTable { +// if zone.abbreviation.caseInsensitiveCompare(abbreviation) == .orderedSame { +// if zone.offsetHours < 0 { +// return (zone.offsetHours * 3600) - (zone.offsetMinutes * 60) +// } +// return (zone.offsetHours * 3600) + (zone.offsetMinutes * 60) +// } +// } +// return 0 +//} +// +//func offsetInSeconds(forOffsetCharacters timeZoneCharacters: String) -> Int { +// let isPlus = timeZoneCharacters.hasPrefix("+") +// let numericValue = timeZoneCharacters.filter { $0.isNumber || $0 == "-" } +// let (hours, finalIndex) = nextNumericValue(bytes: numericValue, startingIndex: 0, maximumNumberOfDigits: 2) +// let (minutes, _) = nextNumericValue(bytes: numericValue, startingIndex: finalIndex + 1, maximumNumberOfDigits: 2) +// +// let seconds = ((hours ?? 0) * 3600) + ((minutes ?? 0) * 60) +// return isPlus ? seconds : -seconds +//} +// +//func parsedTimeZoneOffset(bytes: String, startingIndex: Int) -> Int { +// var timeZoneCharacters: String = "" +// var numberOfCharactersFound = 0 +// var i = startingIndex +// +// while i < bytes.count, numberOfCharactersFound < 5 { +// let character = bytes[bytes.index(bytes.startIndex, offsetBy: i)] +// if character != ":" && character != " " { +// timeZoneCharacters.append(character) +// numberOfCharactersFound += 1 +// } +// i += 1 +// } +// +// if numberOfCharactersFound < 1 || timeZoneCharacters.lowercased() == "z" { +// return 0 +// } +// +// if timeZoneCharacters.range(of: "GMT", options: .caseInsensitive) != nil || +// timeZoneCharacters.range(of: "UTC", options: .caseInsensitive) != nil { +// return 0 +// } +// +// if hasAtLeastOneAlphaCharacter(timeZoneCharacters) { +// return offsetInSeconds(forTimeZoneAbbreviation: timeZoneCharacters) +// } +// return offsetInSeconds(forOffsetCharacters: timeZoneCharacters) +//} +// +//func dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset( +// year: Int, month: Int, day: Int, +// hour: Int, minute: Int, second: Int, +// milliseconds: Int, timeZoneOffset: Int) -> Date? { +// +// var dateComponents = DateComponents() +// dateComponents.year = year +// dateComponents.month = month +// dateComponents.day = day +// dateComponents.hour = hour +// dateComponents.minute = minute +// dateComponents.second = second +// dateComponents.timeZone = TimeZone(secondsFromGMT: timeZoneOffset) +// +// let calendar = Calendar.current +// return calendar.date(from: dateComponents) +//} +// +//func parsePubDate(bytes: String) -> Date? { +// let (day, finalIndex) = nextNumericValue(bytes: bytes, startingIndex: 0, maximumNumberOfDigits: 2) +// let (month, finalIndex2) = nextMonthValue(bytes: bytes, startingIndex: finalIndex + 1) +// let (year, finalIndex3) = nextNumericValue(bytes: bytes, startingIndex: finalIndex2 + 1, maximumNumberOfDigits: 4) +// let (hour, finalIndex4) = nextNumericValue(bytes: bytes, startingIndex: finalIndex3 + 1, maximumNumberOfDigits: 2) +// let (minute, finalIndex5) = nextNumericValue(bytes: bytes, startingIndex: finalIndex4 + 1, maximumNumberOfDigits: 2) +// +// var second = 0 +// let currentIndex = finalIndex5 + 1 +// if currentIndex < bytes.count, bytes[bytes.index(bytes.startIndex, offsetBy: currentIndex)] == ":" { +// second = nextNumericValue(bytes: bytes, startingIndex: currentIndex, maximumNumberOfDigits: 2).0 ?? 0 +// } +// +// let timeZoneOffset = parsedTimeZoneOffset(bytes: bytes, startingIndex: currentIndex + 1) +// +// return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset( +// year: year ?? 1970, +// month: month ?? RSMonth.January.rawValue, +// day: day ?? 1, +// hour: hour ?? 0, +// minute: minute ?? 0, +// second: second, +// milliseconds: 0, +// timeZoneOffset: timeZoneOffset +// ) +//} +// +//func parseW3C(bytes: String) -> Date? { +// let (year, finalIndex) = nextNumericValue(bytes: bytes, startingIndex: 0, maximumNumberOfDigits: 4) +// let (month, finalIndex2) = nextNumericValue(bytes: bytes, startingIndex: finalIndex + 1, maximumNumberOfDigits: 2) +// let (day, finalIndex3) = nextNumericValue(bytes: bytes, startingIndex: finalIndex2 + 1, maximumNumberOfDigits: 2) +// let (hour, finalIndex4) = nextNumericValue(bytes: bytes, startingIndex: finalIndex3 + 1, maximumNumberOfDigits: 2) +// let (minute, finalIndex5) = nextNumericValue(bytes: bytes, startingIndex: finalIndex4 + 1, maximumNumberOfDigits: 2) +// let (second, finalIndex6) = nextNumericValue(bytes: bytes, startingIndex: finalIndex5 + 1, maximumNumberOfDigits: 2) +// +// var milliseconds = 0 +// let currentIndex = finalIndex6 + 1 +// if currentIndex < bytes.count, bytes[bytes.index(bytes.startIndex, offsetBy: currentIndex)] == "." { +// milliseconds = nextNumericValue(bytes: bytes, startingIndex: currentIndex + 1, maximumNumberOfDigits: 3).0 ?? 0 +// } +// +// let timeZoneOffset = parsedTimeZoneOffset(bytes: bytes, startingIndex: currentIndex + 1) +// +// return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset( +// year: year ?? 1970, +// month: month ?? RSMonth.January.rawValue, +// day: day ?? 1, +// hour: hour ?? 0, +// minute: minute ?? 0, +// second: second ?? 0, +// milliseconds: milliseconds, +// timeZoneOffset: timeZoneOffset +// ) +//} +// +//func dateWithBytes(bytes: String) -> Date? { +// guard !bytes.isEmpty else { return nil } +// +// if bytes.range(of: "-") != nil { +// return parseW3C(bytes: bytes) +// } +// return parsePubDate(bytes: bytes) +//} diff --git a/Modules/Parser/Sources/FeedParser/Feeds/FeedParser.swift b/Modules/Parser/Sources/FeedParser/Feeds/FeedParser.swift index a9417e31f..d1e78b464 100644 --- a/Modules/Parser/Sources/FeedParser/Feeds/FeedParser.swift +++ b/Modules/Parser/Sources/FeedParser/Feeds/FeedParser.swift @@ -7,6 +7,7 @@ // import Foundation +import SAX // FeedParser handles RSS, Atom, JSON Feed, and RSS-in-JSON. // You don’t need to know the type of feed. diff --git a/Modules/Parser/Sources/FeedParser/Feeds/FeedType.swift b/Modules/Parser/Sources/FeedParser/Feeds/FeedType.swift index 4dcaaa02c..8cf33225a 100644 --- a/Modules/Parser/Sources/FeedParser/Feeds/FeedType.swift +++ b/Modules/Parser/Sources/FeedParser/Feeds/FeedType.swift @@ -7,6 +7,7 @@ // import Foundation +import SAX public enum FeedType: Sendable { case rss diff --git a/Modules/Parser/Sources/FeedParser/Feeds/JSON/JSONFeedParser.swift b/Modules/Parser/Sources/FeedParser/Feeds/JSON/JSONFeedParser.swift index 0e765961d..247c18612 100644 --- a/Modules/Parser/Sources/FeedParser/Feeds/JSON/JSONFeedParser.swift +++ b/Modules/Parser/Sources/FeedParser/Feeds/JSON/JSONFeedParser.swift @@ -7,6 +7,7 @@ // import Foundation +import SAX // See https://jsonfeed.org/version/1.1 diff --git a/Modules/Parser/Sources/FeedParser/Feeds/JSON/RSSInJSONParser.swift b/Modules/Parser/Sources/FeedParser/Feeds/JSON/RSSInJSONParser.swift index 74e6b0658..e27c0e629 100644 --- a/Modules/Parser/Sources/FeedParser/Feeds/JSON/RSSInJSONParser.swift +++ b/Modules/Parser/Sources/FeedParser/Feeds/JSON/RSSInJSONParser.swift @@ -7,6 +7,7 @@ // import Foundation +import SAX // See https://github.com/scripting/Scripting-News/blob/master/rss-in-json/README.md // Also: http://cyber.harvard.edu/rss/rss.html diff --git a/Modules/Parser/Sources/FeedParser/Feeds/ParsedItem.swift b/Modules/Parser/Sources/FeedParser/Feeds/ParsedItem.swift index 40c719be1..c9fc2eeb8 100644 --- a/Modules/Parser/Sources/FeedParser/Feeds/ParsedItem.swift +++ b/Modules/Parser/Sources/FeedParser/Feeds/ParsedItem.swift @@ -8,7 +8,7 @@ import Foundation -public struct ParsedItem: Hashable, Sendable { +public final class ParsedItem: Hashable, Sendable { public let syncServiceID: String? //Nil when not syncing public let uniqueID: String //RSS guid, for instance; may be calculated diff --git a/Modules/Parser/Sources/FeedParser/Feeds/XML/AtomParser.swift b/Modules/Parser/Sources/FeedParser/Feeds/XML/AtomParser.swift index 151349af7..43fee7810 100644 --- a/Modules/Parser/Sources/FeedParser/Feeds/XML/AtomParser.swift +++ b/Modules/Parser/Sources/FeedParser/Feeds/XML/AtomParser.swift @@ -7,6 +7,7 @@ // import Foundation +import SAX // RSSParser wraps the Objective-C RSAtomParser. // diff --git a/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSArticle.swift b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSArticle.swift new file mode 100644 index 000000000..0bfe62cbb --- /dev/null +++ b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSArticle.swift @@ -0,0 +1,111 @@ +// +// RSSArticle.swift +// +// +// Created by Brent Simmons on 8/27/24. +// + +import Foundation +import FoundationExtras + +final class RSSArticle { + + var feedURL: String + + /// An RSS guid, if present, or calculated from other attributes. + /// Should be unique to the feed, but not necessarily unique + /// across different feeds. (Not suitable for a database ID.) + lazy var articleID: String = { + if let guid { + return guid + } + return calculatedArticleID() + }() + + var guid: String? + var title: String? + var body: String? + var link: String? + var permalink: String? + var authors: [RSSAuthor]? + var enclosures: [RSSEnclosure]? + var datePublished: Date? + var dateModified: Date? + var dateParsed: Date + var language: String? + + init(_ feedURL: String) { + self.feedURL = feedURL + self.dateParsed = Date() + } + + func addEnclosure(_ enclosure: RSSEnclosure) { + + if enclosures == nil { + enclosures = [RSSEnclosure]() + } + enclosures!.append(enclosure) + } + + func addAuthor(_ author: RSSAuthor) { + + if authors == nil { + authors = [RSSAuthor]() + } + authors!.append(author) + } +} + +private extension RSSArticle { + + func calculatedArticleID() -> String { + + // Concatenate a combination of properties when no guid. Then hash the result. + // In general, feeds should have guids. When they don't, re-runs are very likely, + // because there's no other 100% reliable way to determine identity. + // This is intended to create an ID unique inside a feed, but not globally unique. + // Not suitable for a database ID, in other words. + + var s = "" + + let datePublishedTimeStampString: String? = { + guard let datePublished else { + return nil + } + return String(format: "%.0f", datePublished.timeIntervalSince1970) + }() + + // Ideally we have a permalink and a pubDate. + // Either one would probably be a good guid, but together they should be rock-solid. + // (In theory. Feeds are buggy, though.) + if let permalink, !permalink.isEmpty, let datePublishedTimeStampString { + s.append(permalink) + s.append(datePublishedTimeStampString) + } + else if let link, !link.isEmpty, let datePublishedTimeStampString { + s.append(link) + s.append(datePublishedTimeStampString) + } + else if let title, !title.isEmpty, let datePublishedTimeStampString { + s.append(title) + s.append(datePublishedTimeStampString) + } + else if let datePublishedTimeStampString { + s.append(datePublishedTimeStampString) + } + else if let permalink, !permalink.isEmpty { + s.append(permalink) + } + else if let link, !link.isEmpty { + s.append(link) + } + else if let title, !title.isEmpty { + s.append(title) + } + else if let body, !body.isEmpty { + s.append(body) + } + + return s.md5String + } +} diff --git a/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSAuthor.swift b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSAuthor.swift new file mode 100644 index 000000000..a153ecb1e --- /dev/null +++ b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSAuthor.swift @@ -0,0 +1,35 @@ +// +// RSSAuthor.swift +// +// +// Created by Brent Simmons on 8/27/24. +// + +import Foundation + +final class RSSAuthor { + + var name: String? + var url: String? + var avatarURL: String? + var emailAddress: String? + + init(name: String?, url: String?, avatarURL: String?, emailAddress: String?) { + self.name = name + self.url = url + self.avatarURL = avatarURL + self.emailAddress = emailAddress + } + + /// Use when the actual property is unknown. Guess based on contents of the string. (This is common with RSS.) + convenience init(singleString: String) { + + if singleString.contains("@") { + self.init(name: nil, url: nil, avatarURL: nil, emailAddress: singleString) + } else if singleString.lowercased().hasPrefix("http") { + self.init(name: nil, url: singleString, avatarURL: nil, emailAddress: nil) + } else { + self.init(name: singleString, url: nil, avatarURL: nil, emailAddress: nil) + } + } +} diff --git a/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSEnclosure.swift b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSEnclosure.swift new file mode 100644 index 000000000..a427475c8 --- /dev/null +++ b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSEnclosure.swift @@ -0,0 +1,20 @@ +// +// RSSEnclosure.swift +// +// +// Created by Brent Simmons on 8/27/24. +// + +import Foundation + +final class RSSEnclosure { + + var url: String + var length: Int? + var mimeType: String? + var title: String? + + init(url: String) { + self.url = url + } +} diff --git a/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSFeed.swift b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSFeed.swift new file mode 100644 index 000000000..34a334d3b --- /dev/null +++ b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSFeed.swift @@ -0,0 +1,22 @@ +// +// RSSFeed.swift +// +// +// Created by Brent Simmons on 8/27/24. +// + +import Foundation + +final class RSSFeed { + + var urlString: String + var title: String? + var link: String? + var language: String? + + var articles: [RSSArticle]? + + init(urlString: String) { + self.urlString = urlString + } +} diff --git a/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSParser.swift b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSParser.swift index 6a643d516..0f422c27e 100644 --- a/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSParser.swift +++ b/Modules/Parser/Sources/FeedParser/Feeds/XML/RSSParser.swift @@ -11,12 +11,205 @@ import SAX public final class RSSParser { - private var parseFeed: ParsedFeed? + private var parserData: ParserData + private var feedURL: String { + parserData.url + } + private var data: Data { + parserData.data + } + + private let feed: RSSFeed + private var articles = [RSSArticle]() + private var currentArticle: RSSArticle? { + articles.last + } - public static func parsedFeed(with parserData: ParserData) -> ParsedFeed? { + private var endRSSFound = false + private var isRDF = false + private var parsingArticle = false + private var parsingChannelImage = false + private var parsingAuthor = false + private var currentAttributes: XMLAttributesDictionary? + + public static func parsedFeed(with parserData: ParserData) -> RSSFeed { let parser = RSSParser(parserData) parser.parse() - return parser.parsedFeed + return parser.feed + } + + init(_ parserData: ParserData) { + self.parserData = parserData + self.feed = RSSFeed(urlString: parserData.url) } } + +private extension RSSParser { + + private struct XMLName { + static let uppercaseRDF = "RDF".utf8CString + static let item = "item".utf8CString + static let guid = "guid".utf8CString + static let enclosure = "enclosure".utf8CString + static let rdfAbout = "rdf:about".utf8CString + static let image = "image".utf8CString + static let author = "author".utf8CString + static let rss = "rss".utf8CString + static let link = "link".utf8CString + static let title = "title".utf8CString + static let language = "language".utf8CString + static let dc = "dc".utf8CString + static let content = "content".utf8CString + static let encoded = "encoded".utf8CString + } + + func addFeedElement(_ localName: XMLPointer, _ prefix: XMLPointer?) { + + guard prefix == nil else { + return + } + + if SAXEqualTags(localName, XMLName.link) { + if feed.link == nil { + feed.link = currentString + } + } + else if SAXEqualTags(localName, XMLName.title) { + feed.title = currentString + } + else if SAXEqualTags(localName, XMLName.language) { + feed.language = currentString + } + } + + func addArticle() { + let article = RSSArticle(feedURL) + articles.append(article) + } + + func addArticleElement(_ localName: XMLPointer, _ prefix: XMLPointer?) { + + if SAXEqualTags(prefix, XMLName.dc) { + addDCElement(localName) + return; + } + + if SAXEqualTags(prefix, XMLName.content) && SAXEqualTags(localName, XMLName.encoded) { + if let currentString, !currentString.isEmpty { + currentArticle.body = currentString + } + return + } + + guard prefix == nil else { + return + } + + if SAXEqualTags(localName, XMLName.guid) { + addGuid() + } + else if SAXEqualTags(localName, XMLName.pubDate) { + currentArticle.datePublished = currentDate + } + else if SAXEqualTags(localName, XMLName.author) { + addAuthorWithString(currentString) + } + else if SAXEqualTags(localName, XMLName.link) { + currentArticle.link = urlString(currentString) + } + else if SAXEqualTags(localName, XMLName.description) { + if currentArticle.body == nil { + currentArticle.body = currentString + } + } + else if !parsingAuthor && SAXEqualTags(localName, XMLName.title) { + if let currentString { + currentArticle.title = currentString + } + } + else if SAXEqualTags(localName, XMLName.enclosure) { + addEnclosure() + } + } +} + +extension RSSParser: SAXParserDelegate { + + public func saxParser(_ saxParser: SAXParser, xmlStartElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?, namespaceCount: Int, namespaces: UnsafePointer?, attributeCount: Int, attributesDefaultedCount: Int, attributes: UnsafePointer?) { + + if endRSSFound { + return + } + + if SAXEqualTags(localName, XMLName.uppercaseRDF) { + isRDF = true + return + } + + var xmlAttributes: XMLAttributesDictionary? = nil + if (isRDF && SAXEqualTags(localName, XMLName.item)) || SAXEqualTags(localName, XMLName.guid) || SAXEqualTags(enclosure, XMLName.enclosure) { + xmlAttributes = saxParser.attributesDictionary(attributes, attributeCount: attributeCount) + } + if currentAttributes != xmlAttributes { + currentAttributes = xmlAttributes + } + + if prefix == nil && SAXEqualTags(localName, XMLName.item) { + addArticle() + parsingArticle = true + + if isRDF && let rdfGuid = xmlAttributes?[XMLName.rdfAbout], let currentArticle { // RSS 1.0 guid + currentArticle.guid = rdfGuid + currentArticle.permalink = rdfGuid + } + } + else if prefix == nil && SAXEqualTags(localName, XMLName.image) { + parsingChannelImage = true + } + else if prefix == nil && SAXEqualTags(localName, XMLName.author) { + if parsingArticle { + parsingAuthor = true + } + } + + if !parsingChannelImage { + saxParser.beginStoringCharacters() + } + } + + public func saxParser(_ saxParser: SAXParser, xmlEndElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?) { + + if endRSSFound { + return + } + + if isRDF && SAXEqualTags(localName, XMLName.uppercaseRDF) { + endRSSFound = true + } + else if SAXEqualTags(localName, XMLName.rss) { + endRSSFound = true + } + else if SAXEqualTags(localName, XMLName.image) { + parsingChannelImage = false + } + else if SAXEqualTags(localName, XMLName.item) { + parsingArticle = false + } + else if parsingArticle { + addArticleElement(localName, prefix) + if SAXEqualTags(localName, XMLName.author) { + parsingAuthor = false + } + } + else if !parsingChannelImage { + addFeedElement(localName, prefix) + } + } + + public func saxParser(_ saxParser: SAXParser, xmlCharactersFound: XMLPointer, count: Int) { + + // Required method. + } +} + diff --git a/Modules/Parser/Sources/OPMLParser/OPMLParser.swift b/Modules/Parser/Sources/OPMLParser/OPMLParser.swift index b7f44828f..ab21c4185 100644 --- a/Modules/Parser/Sources/OPMLParser/OPMLParser.swift +++ b/Modules/Parser/Sources/OPMLParser/OPMLParser.swift @@ -22,11 +22,6 @@ public final class OPMLParser { itemStack.last } - struct XMLKey { - static let title = "title".utf8CString - static let outline = "outline".utf8CString - } - /// Returns nil if data can’t be parsed (if it’s not OPML). public static func document(with parserData: ParserData) -> OPMLDocument? { @@ -36,7 +31,6 @@ public final class OPMLParser { } init(_ parserData: ParserData) { - self.parserData = parserData } } @@ -79,14 +73,19 @@ private extension OPMLParser { extension OPMLParser: SAXParserDelegate { + private struct XMLName { + static let title = "title".utf8CString + static let outline = "outline".utf8CString + } + public func saxParser(_ saxParser: SAXParser, xmlStartElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?, namespaceCount: Int, namespaces: UnsafePointer?, attributeCount: Int, attributesDefaultedCount: Int, attributes: UnsafePointer?) { - if SAXEqualTags(localName, XMLKey.title) { + if SAXEqualTags(localName, XMLName.title) { saxParser.beginStoringCharacters() return } - if !SAXEqualTags(localName, XMLKey.outline) { + if !SAXEqualTags(localName, XMLName.outline) { return } @@ -99,7 +98,7 @@ extension OPMLParser: SAXParserDelegate { public func saxParser(_ saxParser: SAXParser, xmlEndElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?) { - if SAXEqualTags(localName, XMLKey.title) { + if SAXEqualTags(localName, XMLName.title) { if let item = currentItem as? OPMLDocument { item.title = saxParser.currentStringWithTrimmedWhitespace } @@ -107,7 +106,7 @@ extension OPMLParser: SAXParserDelegate { return } - if SAXEqualTags(localName, XMLKey.outline) { + if SAXEqualTags(localName, XMLName.outline) { popItem() } } diff --git a/Modules/Parser/Sources/SAX/SAXParser.swift b/Modules/Parser/Sources/SAX/SAXParser.swift index cc0ec9943..a9e93bd9f 100644 --- a/Modules/Parser/Sources/SAX/SAXParser.swift +++ b/Modules/Parser/Sources/SAX/SAXParser.swift @@ -91,7 +91,9 @@ public final class SAXParser { characters.count = 0 } - public func attributesDictionary(_ attributes: UnsafePointer?, attributeCount: Int) -> [String: String]? { + public typealias XMLAttributesDictionary = [String: String] + + public func attributesDictionary(_ attributes: UnsafePointer?, attributeCount: Int) -> XMLAttributesDictionary? { guard attributeCount > 0, let attributes else { return nil diff --git a/Modules/Parser/Tests/DateParserTests/DateParserTests.swift b/Modules/Parser/Tests/DateParserTests/DateParserTests.swift new file mode 100644 index 000000000..32f6b5d06 --- /dev/null +++ b/Modules/Parser/Tests/DateParserTests/DateParserTests.swift @@ -0,0 +1,116 @@ +// +// RSDateParserTests.swift +// +// +// Created by Maurice Parker on 4/1/21. +// + +import Foundation +import XCTest +@testable import DateParser + +class DateParserTests: XCTestCase { + + func dateWithValues(_ year: Int, _ month: Int, _ day: Int, _ hour: Int, _ minute: Int, _ second: Int) -> Date { + var dateComponents = DateComponents() + dateComponents.calendar = Calendar.current + dateComponents.timeZone = TimeZone(secondsFromGMT: 0) + + dateComponents.year = year + dateComponents.month = month + dateComponents.day = day + dateComponents.hour = hour + dateComponents.minute = minute + dateComponents.second = second + + return dateComponents.date! + } + + func testDateWithString() { + var expectedDateResult = dateWithValues(2010, 5, 28, 21, 3, 38) + + var d = date("Fri, 28 May 2010 21:03:38 +0000") + XCTAssertEqual(d, expectedDateResult) + + d = date("Fri, 28 May 2010 21:03:38 +00:00") + XCTAssertEqual(d, expectedDateResult) + + d = date("Fri, 28 May 2010 21:03:38 -00:00") + XCTAssertEqual(d, expectedDateResult) + + d = date("Fri, 28 May 2010 21:03:38 -0000") + XCTAssertEqual(d, expectedDateResult) + + d = date("Fri, 28 May 2010 21:03:38 GMT") + XCTAssertEqual(d, expectedDateResult) + + d = date("2010-05-28T21:03:38+00:00") + XCTAssertEqual(d, expectedDateResult) + + d = date("2010-05-28T21:03:38+0000") + XCTAssertEqual(d, expectedDateResult) + + d = date("2010-05-28T21:03:38-0000") + XCTAssertEqual(d, expectedDateResult) + + d = date("2010-05-28T21:03:38-00:00") + XCTAssertEqual(d, expectedDateResult) + + d = date("2010-05-28T21:03:38Z") + XCTAssertEqual(d, expectedDateResult) + + expectedDateResult = dateWithValues(2010, 7, 13, 17, 6, 40) + d = date("2010-07-13T17:06:40+00:00") + XCTAssertEqual(d, expectedDateResult) + + expectedDateResult = dateWithValues(2010, 4, 30, 12, 0, 0) + d = date("30 Apr 2010 5:00 PDT") + XCTAssertEqual(d, expectedDateResult) + + expectedDateResult = dateWithValues(2010, 5, 21, 21, 22, 53) + d = date("21 May 2010 21:22:53 GMT") + XCTAssertEqual(d, expectedDateResult) + + expectedDateResult = dateWithValues(2010, 6, 9, 5, 0, 0) + d = date("Wed, 09 Jun 2010 00:00 EST") + XCTAssertEqual(d, expectedDateResult) + + expectedDateResult = dateWithValues(2010, 6, 23, 3, 43, 50) + d = date("Wed, 23 Jun 2010 03:43:50 Z") + XCTAssertEqual(d, expectedDateResult) + + expectedDateResult = dateWithValues(2010, 6, 22, 3, 57, 49) + d = date("2010-06-22T03:57:49+00:00") + XCTAssertEqual(d, expectedDateResult) + + expectedDateResult = dateWithValues(2010, 11, 17, 13, 40, 07) + d = date("2010-11-17T08:40:07-05:00") + XCTAssertEqual(d, expectedDateResult) + } + + func testAtomDateWithMissingTCharacter() { + let expectedDateResult = dateWithValues(2010, 11, 17, 13, 40, 07) + let d = date("2010-11-17 08:40:07-05:00") + XCTAssertEqual(d, expectedDateResult) + } + + func testFeedbinDate() { + let expectedDateResult = dateWithValues(2019, 9, 27, 21, 01, 48) + let d = date("2019-09-27T21:01:48.000000Z") + XCTAssertEqual(d, expectedDateResult) + } + + func testHighMillisecondDate() { + let expectedDateResult = dateWithValues(2021, 03, 29, 10, 46, 56) + let d = date("2021-03-29T10:46:56.516941+00:00") + XCTAssertEqual(d, expectedDateResult) + } +} + +private extension DateParserTests { + + func date(_ string: String) -> Date? { + let d = Data(string.utf8) + return Date(data: d) + } +} diff --git a/Modules/Parser/Tests/FeedParserTests/RSDateParserTests.swift b/Modules/Parser/Tests/FeedParserTests/RSDateParserTests.swift deleted file mode 100644 index e9984933c..000000000 --- a/Modules/Parser/Tests/FeedParserTests/RSDateParserTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// RSDateParserTests.swift -// -// -// Created by Maurice Parker on 4/1/21. -// - -import Foundation -import XCTest -import Parser -import ParserObjC - -class RSDateParserTests: XCTestCase { - - static func dateWithValues(_ year: Int, _ month: Int, _ day: Int, _ hour: Int, _ minute: Int, _ second: Int) -> Date { - var dateComponents = DateComponents() - dateComponents.calendar = Calendar.current - dateComponents.timeZone = TimeZone(secondsFromGMT: 0) - - dateComponents.year = year - dateComponents.month = month - dateComponents.day = day - dateComponents.hour = hour - dateComponents.minute = minute - dateComponents.second = second - - return dateComponents.date! - } - - func testDateWithString() { - var expectedDateResult = Self.dateWithValues(2010, 5, 28, 21, 3, 38) - - var d = RSDateWithString("Fri, 28 May 2010 21:03:38 +0000") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("Fri, 28 May 2010 21:03:38 +00:00") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("Fri, 28 May 2010 21:03:38 -00:00") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("Fri, 28 May 2010 21:03:38 -0000") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("Fri, 28 May 2010 21:03:38 GMT") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("2010-05-28T21:03:38+00:00") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("2010-05-28T21:03:38+0000") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("2010-05-28T21:03:38-0000") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("2010-05-28T21:03:38-00:00") - XCTAssertEqual(d, expectedDateResult) - - d = RSDateWithString("2010-05-28T21:03:38Z") - XCTAssertEqual(d, expectedDateResult) - - expectedDateResult = Self.dateWithValues(2010, 7, 13, 17, 6, 40) - d = RSDateWithString("2010-07-13T17:06:40+00:00") - XCTAssertEqual(d, expectedDateResult) - - expectedDateResult = Self.dateWithValues(2010, 4, 30, 12, 0, 0) - d = RSDateWithString("30 Apr 2010 5:00 PDT") - XCTAssertEqual(d, expectedDateResult) - - expectedDateResult = Self.dateWithValues(2010, 5, 21, 21, 22, 53) - d = RSDateWithString("21 May 2010 21:22:53 GMT") - XCTAssertEqual(d, expectedDateResult) - - expectedDateResult = Self.dateWithValues(2010, 6, 9, 5, 0, 0) - d = RSDateWithString("Wed, 09 Jun 2010 00:00 EST") - XCTAssertEqual(d, expectedDateResult) - - expectedDateResult = Self.dateWithValues(2010, 6, 23, 3, 43, 50) - d = RSDateWithString("Wed, 23 Jun 2010 03:43:50 Z") - XCTAssertEqual(d, expectedDateResult) - - expectedDateResult = Self.dateWithValues(2010, 6, 22, 3, 57, 49) - d = RSDateWithString("2010-06-22T03:57:49+00:00") - XCTAssertEqual(d, expectedDateResult) - - expectedDateResult = Self.dateWithValues(2010, 11, 17, 13, 40, 07) - d = RSDateWithString("2010-11-17T08:40:07-05:00") - XCTAssertEqual(d, expectedDateResult) - } - - func testAtomDateWithMissingTCharacter() { - let expectedDateResult = Self.dateWithValues(2010, 11, 17, 13, 40, 07) - let d = RSDateWithString("2010-11-17 08:40:07-05:00") - XCTAssertEqual(d, expectedDateResult) - } - - func testFeedbinDate() { - let expectedDateResult = Self.dateWithValues(2019, 9, 27, 21, 01, 48) - let d = RSDateWithString("2019-09-27T21:01:48.000000Z") - XCTAssertEqual(d, expectedDateResult) - } - -// func testHighMillisecondDate() { -// let expectedDateResult = Self.dateWithValues(2021, 03, 29, 10, 46, 56) -// let d = RSDateWithString("2021-03-29T10:46:56.516941+00:00") -// XCTAssertEqual(d, expectedDateResult) -// } -}