212 lines
5.3 KiB
Swift
212 lines
5.3 KiB
Swift
import Foundation
|
|
|
|
/// Represents a number that can be checked if it is within an acceptable range. It can generate errors or warnings depending
|
|
/// on the current value.
|
|
///
|
|
@dynamicMemberLookup
|
|
public struct Flagged: Equatable, Sendable {
|
|
|
|
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 @Sendable (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: Sendable {
|
|
let message: @Sendable (Double) -> String?
|
|
|
|
public init(message: @escaping @Sendable (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: Sendable {
|
|
|
|
let checkValue: @Sendable (Double) -> CheckResult
|
|
|
|
public init(_ checkValue: @escaping @Sendable (Double) -> CheckResult) {
|
|
self.checkValue = checkValue
|
|
}
|
|
|
|
public func callAsFunction(_ value: Double) -> CheckResult {
|
|
self.checkValue(value)
|
|
}
|
|
}
|
|
|
|
public enum CheckResult: Equatable, Sendable {
|
|
|
|
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, Sendable {
|
|
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<T>(dynamicMember keyPath: KeyPath<CheckResult, T>) -> 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: EquipmentMetadata.CoolingCapacity,
|
|
ratings: RatedAirflowPerTon = .init(),
|
|
goodMessage: Flagged.GoodMessageHandler? = nil
|
|
) -> Self {
|
|
.rated(RatedAirflowLimits(tons: tons, airflowPerTon: ratings), goodMessage: goodMessage)
|
|
}
|
|
|
|
public static func percent(
|
|
goodMessage: Flagged.GoodMessageHandler = .none
|
|
) -> Self {
|
|
.using(maximum: 100, minimum: 100, rated: 100)
|
|
}
|
|
|
|
public static func rated<T>(
|
|
_ ratings: RatedEnvelope<T>,
|
|
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)"
|
|
}
|