Created
October 12, 2016 17:40
-
-
Save joshavant/3b848171624560b2545011afc10924aa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum Result<T,U> { | |
case firstType(T) | |
case secondType(U) | |
} | |
enum Validation<T,Error> { | |
case valid(T) | |
case invalid(Error) | |
init(input: T, comparison: ((T) -> (Result<T,Error>))) { | |
switch comparison(input) { | |
case .firstType(let value): | |
self = .valid(value) | |
case .secondType(let error): | |
self = .invalid(error) | |
} | |
} | |
} | |
////////// | |
// Data Structure: | |
struct Birthdate { | |
enum Component { | |
case month | |
case day | |
case year | |
func debugDescription() -> String { | |
switch self { | |
case .month: return "Month" | |
case .day: return "Day" | |
case .year: return "Year" | |
} | |
} | |
} | |
let month: Int? | |
let day: Int? | |
let year: Int? | |
enum DateGenerationError: Error { | |
case missingComponents(Set<Component>) | |
case couldntGenerateDateFromComponents | |
} | |
func generateDate() throws -> Date { | |
guard | |
let aMonth = self.month, | |
let aDay = self.day, | |
let aYear = self.year | |
else { | |
var components = Set<Component>() | |
if self.month == nil { components.insert(.month) } | |
if self.day == nil { components.insert(.day) } | |
if self.year == nil { components.insert(.year) } | |
throw DateGenerationError.missingComponents(components) | |
} | |
var calendar = Calendar(identifier: .gregorian) | |
calendar.locale = Locale.autoupdatingCurrent | |
guard let generatedDate = DateComponents(calendar: calendar, | |
timeZone: nil, | |
era: nil, | |
year: aYear, | |
month: aMonth, | |
day: aDay, | |
hour: nil, | |
minute: nil, | |
second: nil, | |
nanosecond: nil, | |
weekday: nil, | |
weekdayOrdinal: nil, | |
quarter: nil, | |
weekOfMonth: nil, | |
weekOfYear: nil, | |
yearForWeekOfYear: nil).date | |
else { throw DateGenerationError.couldntGenerateDateFromComponents } | |
return generatedDate | |
} | |
func under18Threshold() -> Date { | |
return Date(timeIntervalSinceNow: -1 * (60 * 60 * 24 * 365 * 18)) | |
} | |
func under21Threshold() -> Date { | |
return Date(timeIntervalSinceNow: -1 * (60 * 60 * 24 * 365 * 21)) | |
} | |
} | |
////////// | |
// Validation Pieces: | |
enum BirthdateValidationError { | |
struct AgeLevels: OptionSet { | |
let rawValue: Int | |
static let under18 = AgeLevels(rawValue: 1 << 0) | |
static let under21 = AgeLevels(rawValue: 1 << 1) | |
} | |
case missingComponents(Set<Birthdate.Component>) | |
case componentsDontFormValidDate | |
case underage(AgeLevels) | |
} | |
let validator: ((Birthdate) -> (Result<Birthdate,BirthdateValidationError>)) = { | |
birthdate in | |
do { | |
let date = try birthdate.generateDate() | |
let under18Comparison = date.compare(birthdate.under18Threshold()) | |
let under21Comparison = date.compare(birthdate.under21Threshold()) | |
switch (under18Comparison, under21Comparison) { | |
case (.orderedDescending, _): | |
return .secondType(.underage(.under18)) | |
case (.orderedSame, _), | |
(.orderedAscending, .orderedDescending), | |
(.orderedAscending, .orderedSame): | |
return .secondType(.underage(.under21)) | |
case (.orderedAscending, .orderedAscending): | |
return .firstType(birthdate) | |
} | |
} catch Birthdate.DateGenerationError.missingComponents(let components) { | |
return .secondType(.missingComponents(components)) | |
} catch { | |
return .secondType(.componentsDontFormValidDate) | |
} | |
} | |
// Prints debug string for result | |
func parseValidationResult(_ result: Validation<Birthdate,BirthdateValidationError>) { | |
switch result { | |
case .valid(let birthdate): | |
let date = try? birthdate.generateDate() | |
print("Valid: \(date)") | |
case .invalid(let error): | |
switch error { | |
case .missingComponents(let components): | |
let componentsList = components.map({ $0.debugDescription() }).joined(separator: " ") | |
print("Missing Components: \(componentsList)") | |
case .componentsDontFormValidDate: | |
print("Components don't form valid date") | |
case .underage(let ageLevels): | |
var message = "Underage:" | |
if ageLevels.contains(.under21) { | |
message += " Under 21" | |
} else if ageLevels.contains(.under18) { | |
message += " Under 18" | |
} | |
print(message) | |
} | |
} | |
} | |
let missingMonthBirthdate = Birthdate(month: nil, day: 1, year: nil) | |
let missingMonthValidationResult = Validation<Birthdate,BirthdateValidationError>(input: missingMonthBirthdate, comparison: validator) | |
parseValidationResult(missingMonthValidationResult) // Missing Components: Year Month | |
// So, generating an invalid date from DateComponents(calendar:...) is actually pretty hard and I couldn't figure out a way to do it. :) | |
let under18Birthdate = Birthdate(month: 1, day: 2, year: 1997) | |
let under18ValidationResult = Validation<Birthdate,BirthdateValidationError>(input: under18Birthdate, comparison: validator) | |
parseValidationResult(under18ValidationResult) // Underage: Under 21 | |
let under21Birthdate = Birthdate(month: 10, day: 16, year: 2016) | |
let under21ValidationResult = Validation<Birthdate,BirthdateValidationError>(input: under21Birthdate, comparison: validator) | |
parseValidationResult(under21ValidationResult) // Underage: Under 18 | |
let validBirthdate = Birthdate(month: 1, day: 2, year: 1900) | |
let validValidationResult = Validation<Birthdate,BirthdateValidationError>(input: validBirthdate, comparison: validator) | |
parseValidationResult(validValidationResult) // Valid: Optional(1900-01-02 08:00:00 +0000) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment