import Foundation @dynamicMemberLookup public struct Flagged: Equatable { public var checkValue: CheckHandler public var wrappedValue: Double public init( wrappedValue: Double, _ handler: CheckHandler ) { self.checkValue = handler self.wrappedValue = wrappedValue } public init( wrappedValue: Double, _ checkValue: @escaping (Double) -> CheckResult ) { self.checkValue = .init(checkValue) self.wrappedValue = wrappedValue } public static func error(_ wrappedValue: Double? = nil, message: String) -> Self { self.init(wrappedValue: wrappedValue ?? 0) { _ in return .error(message) } } public var projectedValue: CheckResult { checkValue(wrappedValue) } public struct GoodMessageHandler { let message: (Double) -> String? public init(message: @escaping (Double) -> String?) { self.message = message } public func callAsFunction(_ double: Double) -> String? { self.message(double) } public static var none: Self { .init { _ in nil } } } public struct CheckHandler { let checkValue: (Double) -> CheckResult public init(_ checkValue: @escaping (Double) -> CheckResult) { self.checkValue = checkValue } public func callAsFunction(_ value: Double) -> CheckResult { self.checkValue(value) } } public enum CheckResult: Equatable { case aboveMaximum(Double) case belowMinimum(Double) case betweenRange(minimum: Double, maximum: Double) case betweenRatedAndMaximum(rated: Double, maximum: Double) case good(String? = nil) case error(String) public var status: Status { switch self { case .aboveMaximum(_): return .error case .belowMinimum(_): return .error case .betweenRange: return .warning case .betweenRatedAndMaximum: return .warning case .good(_): return .good case .error(_): return .error } } public var message: String? { switch self { case let .aboveMaximum(max): return "Above maximum: \(doubleString(max))" case let .belowMinimum(min): return "Below minimum: \(doubleString(min))" case .betweenRange(minimum: let minimum, maximum: let maximum): return "Between: \(minimum) and \(maximum)" case .betweenRatedAndMaximum(rated: let rated, maximum: let maximum): return "Between rated: \(doubleString(rated)) and maximum: \(doubleString(maximum))" case let .good(goodMessage): return goodMessage case let .error(errorMessage): return errorMessage } } public enum Status: String, Equatable, CaseIterable { case good, warning, error public var title: String { self.rawValue.capitalized } } } public static func == (lhs: Flagged, rhs: Flagged) -> Bool { lhs.wrappedValue == rhs.wrappedValue } public subscript(dynamicMember keyPath: KeyPath) -> T { self.projectedValue[keyPath: keyPath] } } // MARK: - Check Handlers extension Flagged.CheckHandler { public static func using( maximum: Double, minimum: Double, rated: Double, goodMessage: Flagged.GoodMessageHandler = .none ) -> Self { .init { value in if value < minimum { return .belowMinimum(minimum) } else if value > maximum { return .aboveMaximum(maximum) } else if value > rated { return .betweenRatedAndMaximum(rated: rated, maximum: maximum) } else { return .good(goodMessage(value)) } } } public static func airflow( tons: CoolingCapacity, ratings: RatedAirflowPerTon = .init(), goodMessage: Flagged.GoodMessageHandler? = nil ) -> Self { .rated(RatedAirflowLimits(tons: tons, using: ratings), goodMessage: goodMessage) } public static func percent( goodMessage: Flagged.GoodMessageHandler = .none ) -> Self { .using(maximum: 100, minimum: 100, rated: 100) } public static func rated( _ ratings: RatedEnvelope, goodMessage: Flagged.GoodMessageHandler? = nil ) -> Self { .using( maximum: ratings.maximum, minimum: ratings.minimum, rated: ratings.rated, goodMessage: goodMessage ?? .none ) } public static func result(_ result: Flagged.CheckResult) -> Self { self.init { _ in result } } public static func velocity( goodMessage: Flagged.GoodMessageHandler = .none ) -> Self { .init { value in if value <= 700 { return .good(goodMessage(value)) } else if value < 900 { return .betweenRange(minimum: 700, maximum: 900) } else { return .error("Velocity greater than 900 FPM") } } } } // MARK: - Helpers fileprivate let formatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 2 return formatter }() fileprivate func doubleString(_ double: Double) -> String { return formatter.string(for: double) ?? "\(double)" }