feat: Initial commit
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
42
Package.swift
Normal file
42
Package.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
// swift-tools-version: 5.9
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "swift-estimated-pressures-core",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
.macOS(.v10_15),
|
||||
.tvOS(.v13),
|
||||
.watchOS(.v6),
|
||||
],
|
||||
products: [
|
||||
.library(name: "EstimatedPressureDependency", targets: ["EstimatedPressureDependency"]),
|
||||
.library(name: "SharedModels", targets: ["SharedModels"]),
|
||||
|
||||
],
|
||||
dependencies: [
|
||||
.package(
|
||||
url: "https://github.com/pointfreeco/swift-dependencies.git",
|
||||
from: "1.3.0"
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "EstimatedPressureDependency",
|
||||
dependencies: [
|
||||
"SharedModels",
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
||||
]
|
||||
),
|
||||
.target(name: "SharedModels"),
|
||||
.testTarget(
|
||||
name: "EstimatedPressureTests",
|
||||
dependencies: [
|
||||
"EstimatedPressureDependency",
|
||||
"SharedModels"
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
248
Sources/EstimatedPressureDependency/Key.swift
Normal file
248
Sources/EstimatedPressureDependency/Key.swift
Normal file
@@ -0,0 +1,248 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Foundation
|
||||
@_exported import SharedModels
|
||||
|
||||
extension DependencyValues {
|
||||
|
||||
public var estimatedPressuresClient: EstimatedPressureDependency {
|
||||
get { self[EstimatedPressureDependency.self] }
|
||||
set { self[EstimatedPressureDependency.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct EstimatedPressureDependency {
|
||||
|
||||
public var estimatedAirflow: (EstimatedAirflowRequest) async throws -> Positive<Double>
|
||||
public var estimatedPressure: (EstimatedPressureRequest) async throws -> Positive<Double>
|
||||
|
||||
public struct EstimatedPressureRequest: Equatable {
|
||||
public let existingPressure: Positive<Double>
|
||||
public let existingAirflow: Positive<Double>
|
||||
public let targetAirflow: Positive<Double>
|
||||
|
||||
public init(
|
||||
existingPressure: Double,
|
||||
existingAirflow: Double,
|
||||
targetAirflow: Double
|
||||
) {
|
||||
self.existingPressure = .init(wrappedValue: existingPressure)
|
||||
self.existingAirflow = .init(wrappedValue: existingAirflow)
|
||||
self.targetAirflow = .init(wrappedValue: targetAirflow)
|
||||
}
|
||||
}
|
||||
|
||||
public struct EstimatedAirflowRequest: Equatable {
|
||||
public let existingAirflow: Positive<Double>
|
||||
public let existingPressure: Positive<Double>
|
||||
public let targetPressure: Positive<Double>
|
||||
|
||||
public init(
|
||||
existingAirflow: Double,
|
||||
existingPressure: Double,
|
||||
targetPressure: Double
|
||||
) {
|
||||
self.existingPressure = .init(wrappedValue: existingPressure)
|
||||
self.existingAirflow = .init(wrappedValue: existingAirflow)
|
||||
self.targetPressure = .init(wrappedValue: targetPressure)
|
||||
}
|
||||
}
|
||||
|
||||
public func estimatedPressure(
|
||||
existingPressure: Double,
|
||||
existingAirflow: Double,
|
||||
targetAirflow: Double
|
||||
) async throws -> Positive<Double> {
|
||||
try await self.estimatedPressure(
|
||||
.init(
|
||||
existingPressure: existingPressure,
|
||||
existingAirflow: existingAirflow,
|
||||
targetAirflow: targetAirflow
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension EstimatedPressureDependency {
|
||||
private func estimatedPressure(
|
||||
existingPressure: Positive<Double?>,
|
||||
existingAirflow: Double,
|
||||
targetAirflow: Double
|
||||
) async throws -> Positive<Double> {
|
||||
try await self.estimatedPressure(
|
||||
.init(
|
||||
existingPressure: existingPressure.positiveValue ?? 0,
|
||||
existingAirflow: existingAirflow,
|
||||
targetAirflow: targetAirflow
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public func estimatedPressure(
|
||||
for equipmentMeasurement: EquipmentMeasurement,
|
||||
at upgradedAirflow: Double
|
||||
) async throws -> EquipmentMeasurement {
|
||||
switch equipmentMeasurement {
|
||||
case let .airHandler(airHandler):
|
||||
guard let airflow = airHandler.airflow else {
|
||||
throw InvalidAirflow()
|
||||
}
|
||||
return try await .airHandler(
|
||||
.init(
|
||||
airflow: upgradedAirflow,
|
||||
returnPlenumPressure: self.estimatedPressure(
|
||||
existingPressure: airHandler.$returnPlenumPressure,
|
||||
existingAirflow: airflow,
|
||||
targetAirflow: upgradedAirflow
|
||||
),
|
||||
postFilterPressure: self.estimatedPressure(
|
||||
existingPressure: airHandler.$postFilterPressure,
|
||||
existingAirflow: airflow,
|
||||
targetAirflow: upgradedAirflow
|
||||
),
|
||||
postCoilPressure: self.estimatedPressure(
|
||||
existingPressure: airHandler.$postCoilPressure,
|
||||
existingAirflow: airflow,
|
||||
targetAirflow: upgradedAirflow
|
||||
),
|
||||
supplyPlenumPressure: self.estimatedPressure(
|
||||
existingPressure: airHandler.$supplyPlenumPressure,
|
||||
existingAirflow: airflow,
|
||||
targetAirflow: upgradedAirflow
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
case let .furnaceAndCoil(furnaceAndCoil):
|
||||
guard let airflow = furnaceAndCoil.airflow else {
|
||||
throw InvalidAirflow()
|
||||
}
|
||||
return try await .furnaceAndCoil(
|
||||
.init(
|
||||
airflow: upgradedAirflow,
|
||||
returnPlenumPressure: self.estimatedPressure(
|
||||
existingPressure: furnaceAndCoil.$returnPlenumPressure,
|
||||
existingAirflow: airflow,
|
||||
targetAirflow: upgradedAirflow
|
||||
),
|
||||
postFilterPressure: self.estimatedPressure(
|
||||
existingPressure: furnaceAndCoil.$postFilterPressure,
|
||||
existingAirflow: airflow,
|
||||
targetAirflow: upgradedAirflow
|
||||
),
|
||||
preCoilPressure: self.estimatedPressure(
|
||||
existingPressure: furnaceAndCoil.$preCoilPressure,
|
||||
existingAirflow: airflow,
|
||||
targetAirflow: upgradedAirflow
|
||||
),
|
||||
supplyPlenumPressure: self.estimatedPressure(
|
||||
existingPressure: furnaceAndCoil.$supplyPlenumPressure,
|
||||
existingAirflow: airflow,
|
||||
targetAirflow: upgradedAirflow
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public func estimatedPressure(
|
||||
for equipmentMeasurement: EquipmentMeasurement,
|
||||
at upgradedAirflow: Double,
|
||||
with filterPressureDrop: Positive<Double>
|
||||
) async throws -> EquipmentMeasurement {
|
||||
let estimate = try await estimatedPressure(
|
||||
for: equipmentMeasurement,
|
||||
at: upgradedAirflow
|
||||
)
|
||||
|
||||
switch estimate {
|
||||
|
||||
case var .airHandler(airHandler):
|
||||
airHandler.postFilterPressure = airHandler.returnPlenumPressure + filterPressureDrop.positiveValue
|
||||
return .airHandler(airHandler)
|
||||
|
||||
case var .furnaceAndCoil(furnaceAndCoil):
|
||||
furnaceAndCoil.postFilterPressure = furnaceAndCoil.returnPlenumPressure + filterPressureDrop.positiveValue
|
||||
return .furnaceAndCoil(furnaceAndCoil)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension EstimatedPressureDependency: DependencyKey {
|
||||
|
||||
public static var testValue: EstimatedPressureDependency { Self() }
|
||||
|
||||
public static var liveValue: EstimatedPressureDependency {
|
||||
.init(
|
||||
estimatedAirflow: { request in
|
||||
guard request.existingPressure.positiveValue > 0 else {
|
||||
throw LessThanZeroError.existingPressure
|
||||
}
|
||||
let value = request.existingAirflow.positiveValue * sqrt(
|
||||
request.targetPressure.positiveValue / request.existingPressure.positiveValue
|
||||
)
|
||||
return .init(wrappedValue: value)
|
||||
},
|
||||
estimatedPressure: { request in
|
||||
guard request.existingAirflow.positiveValue > 0 else {
|
||||
throw LessThanZeroError.existingAirflow
|
||||
}
|
||||
|
||||
let value = pow(
|
||||
request.targetAirflow.positiveValue / request.existingAirflow.positiveValue,
|
||||
2
|
||||
) * request.existingPressure.positiveValue
|
||||
|
||||
return .init(wrappedValue: value)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension EquipmentMeasurement.AirHandler {
|
||||
init(
|
||||
airflow: Double? = nil,
|
||||
returnPlenumPressure: Positive<Double>,
|
||||
postFilterPressure: Positive<Double>,
|
||||
postCoilPressure: Positive<Double>,
|
||||
supplyPlenumPressure: Positive<Double>
|
||||
) {
|
||||
self.init(
|
||||
airflow: airflow,
|
||||
returnPlenumPressure: returnPlenumPressure.positiveValue,
|
||||
postFilterPressure: postFilterPressure.positiveValue,
|
||||
postCoilPressure: postCoilPressure.positiveValue,
|
||||
supplyPlenumPressure: supplyPlenumPressure.positiveValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension EquipmentMeasurement.FurnaceAndCoil {
|
||||
init(
|
||||
airflow: Double? = nil,
|
||||
returnPlenumPressure: Positive<Double>,
|
||||
postFilterPressure: Positive<Double>,
|
||||
preCoilPressure: Positive<Double>,
|
||||
supplyPlenumPressure: Positive<Double>
|
||||
) {
|
||||
self.init(
|
||||
airflow: airflow,
|
||||
returnPlenumPressure: returnPlenumPressure.positiveValue,
|
||||
postFilterPressure: postFilterPressure.positiveValue,
|
||||
preCoilPressure: preCoilPressure.positiveValue,
|
||||
supplyPlenumPressure: supplyPlenumPressure.positiveValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct InvalidAirflow: Error { }
|
||||
|
||||
enum LessThanZeroError: Error {
|
||||
case existingAirflow
|
||||
case existingPressure
|
||||
}
|
||||
|
||||
54
Sources/SharedModels/BudgetedPercentEnvelope.swift
Normal file
54
Sources/SharedModels/BudgetedPercentEnvelope.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
|
||||
public struct BudgetedPercentEnvelope: Equatable {
|
||||
|
||||
public var coilBudget: Percentage
|
||||
public var filterBudget: Percentage
|
||||
public var returnPlenumBudget: Percentage
|
||||
public var supplyPlenumBudget: Percentage
|
||||
|
||||
public var preCoilBudget: Percentage {
|
||||
coilBudget + supplyPlenumBudget
|
||||
}
|
||||
|
||||
public init(
|
||||
coilBudget: Percentage,
|
||||
filterBudget: Percentage,
|
||||
returnPlenumBudget: Percentage,
|
||||
supplyPlenumBudget: Percentage
|
||||
) {
|
||||
self.coilBudget = coilBudget
|
||||
self.filterBudget = filterBudget
|
||||
self.returnPlenumBudget = returnPlenumBudget
|
||||
self.supplyPlenumBudget = supplyPlenumBudget
|
||||
}
|
||||
|
||||
public init(equipmentType: EquipmentType, fanType: FanType) {
|
||||
switch equipmentType {
|
||||
case .furnaceAndCoil:
|
||||
switch fanType {
|
||||
case .constantSpeed:
|
||||
self.init(
|
||||
coilBudget: 40%,
|
||||
filterBudget: 20%,
|
||||
returnPlenumBudget: 20%,
|
||||
supplyPlenumBudget: 20%
|
||||
)
|
||||
case .variableSpeed:
|
||||
self.init(
|
||||
coilBudget: 30%,
|
||||
filterBudget: 40%,
|
||||
returnPlenumBudget: 15%,
|
||||
supplyPlenumBudget: 15%
|
||||
)
|
||||
}
|
||||
case .airHandler:
|
||||
self.init(
|
||||
coilBudget: .zero,
|
||||
filterBudget: 50%,
|
||||
returnPlenumBudget: 25%,
|
||||
supplyPlenumBudget: 25%
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Sources/SharedModels/CoolingCapacity.swift
Normal file
16
Sources/SharedModels/CoolingCapacity.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
public enum CoolingCapacity: Double, Equatable, CaseIterable {
|
||||
case half = 0.5
|
||||
case threeQuarter = 0.75
|
||||
case one = 1
|
||||
case oneAndAHalf = 1.5
|
||||
case two = 2
|
||||
case twoAndAHalf = 2.5
|
||||
case three = 3
|
||||
case threeAndAHalf = 3.5
|
||||
case four = 4
|
||||
case five = 5
|
||||
|
||||
public static var `default`: Self { .three }
|
||||
}
|
||||
117
Sources/SharedModels/EquipmentMeasurement.swift
Normal file
117
Sources/SharedModels/EquipmentMeasurement.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
import Foundation
|
||||
|
||||
public enum EquipmentMeasurement: Equatable {
|
||||
|
||||
case airHandler(AirHandler)
|
||||
case furnaceAndCoil(FurnaceAndCoil)
|
||||
|
||||
public var equipmentType: EquipmentType {
|
||||
switch self {
|
||||
case .airHandler:
|
||||
return .airHandler
|
||||
case .furnaceAndCoil:
|
||||
return .furnaceAndCoil
|
||||
}
|
||||
}
|
||||
|
||||
public var externalStaticPressure: Double {
|
||||
switch self {
|
||||
case let .airHandler(airHandler):
|
||||
return airHandler.externalStaticPressure
|
||||
case let .furnaceAndCoil(furnaceAndCoil):
|
||||
return furnaceAndCoil.externalStaticPressure
|
||||
}
|
||||
}
|
||||
|
||||
public struct AirHandler: Equatable {
|
||||
|
||||
public var airflow: Double?
|
||||
|
||||
@Positive
|
||||
public var returnPlenumPressure: Double?
|
||||
|
||||
@Positive
|
||||
public var postFilterPressure: Double?
|
||||
|
||||
@Positive
|
||||
public var postCoilPressure: Double?
|
||||
|
||||
@Positive
|
||||
public var supplyPlenumPressure: Double?
|
||||
|
||||
public init(
|
||||
airflow: Double? = nil,
|
||||
returnPlenumPressure: Double? = nil,
|
||||
postFilterPressure: Double? = nil,
|
||||
postCoilPressure: Double? = nil,
|
||||
supplyPlenumPressure: Double? = nil
|
||||
) {
|
||||
self.airflow = airflow
|
||||
self.returnPlenumPressure = returnPlenumPressure
|
||||
self.postFilterPressure = postFilterPressure
|
||||
self.postCoilPressure = postCoilPressure
|
||||
self.supplyPlenumPressure = supplyPlenumPressure
|
||||
}
|
||||
|
||||
public var externalStaticPressure: Double {
|
||||
($returnPlenumPressure.positiveValue ?? 0) + ($supplyPlenumPressure.positiveValue ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
public struct FurnaceAndCoil: Equatable {
|
||||
|
||||
public var airflow: Double?
|
||||
|
||||
@Positive
|
||||
public var returnPlenumPressure: Double?
|
||||
|
||||
@Positive
|
||||
public var postFilterPressure: Double?
|
||||
|
||||
@Positive
|
||||
public var preCoilPressure: Double?
|
||||
|
||||
@Positive
|
||||
public var supplyPlenumPressure: Double?
|
||||
|
||||
public init(
|
||||
airflow: Double? = nil,
|
||||
returnPlenumPressure: Double? = nil,
|
||||
postFilterPressure: Double? = nil,
|
||||
preCoilPressure: Double? = nil,
|
||||
supplyPlenumPressure: Double? = nil
|
||||
) {
|
||||
self.airflow = airflow
|
||||
self.returnPlenumPressure = returnPlenumPressure
|
||||
self.postFilterPressure = postFilterPressure
|
||||
self.preCoilPressure = preCoilPressure
|
||||
self.supplyPlenumPressure = supplyPlenumPressure
|
||||
}
|
||||
|
||||
public var externalStaticPressure: Double {
|
||||
($postFilterPressure.positiveValue ?? 0) + ($preCoilPressure.positiveValue ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EquipmentMeasurement.AirHandler {
|
||||
|
||||
public enum Key: String, Equatable, CaseIterable {
|
||||
case returnPlenumPressure
|
||||
case postFilterPressure
|
||||
case postCoilPressure
|
||||
case supplyPlenumPressure
|
||||
case airflow
|
||||
}
|
||||
}
|
||||
|
||||
extension EquipmentMeasurement.FurnaceAndCoil {
|
||||
|
||||
public enum Key: String, Equatable, CaseIterable {
|
||||
case returnPlenumPressure
|
||||
case postFilterPressure
|
||||
case preCoilPressure
|
||||
case supplyPlenumPressure
|
||||
case airflow
|
||||
}
|
||||
}
|
||||
16
Sources/SharedModels/EquipmentType.swift
Normal file
16
Sources/SharedModels/EquipmentType.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
public enum EquipmentType: Equatable, CaseIterable, CustomStringConvertible {
|
||||
case airHandler
|
||||
case furnaceAndCoil
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .airHandler:
|
||||
return "Air Handler"
|
||||
|
||||
case .furnaceAndCoil:
|
||||
return "Furnace & Coil"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Sources/SharedModels/FanType.swift
Normal file
15
Sources/SharedModels/FanType.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
public enum FanType: Equatable, CaseIterable, CustomStringConvertible {
|
||||
case constantSpeed
|
||||
case variableSpeed
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .constantSpeed:
|
||||
return "Constant Speed"
|
||||
case .variableSpeed:
|
||||
return "Variable Speed"
|
||||
}
|
||||
}
|
||||
}
|
||||
254
Sources/SharedModels/Flagged.swift
Normal file
254
Sources/SharedModels/Flagged.swift
Normal file
@@ -0,0 +1,254 @@
|
||||
import Foundation
|
||||
|
||||
public enum FlaggedValue<Value> {
|
||||
case good(Value, String?)
|
||||
case warning(Value, String?)
|
||||
case error(Value, String?)
|
||||
|
||||
public init(_ value: Value, key: Key, message: String? = nil) {
|
||||
switch key {
|
||||
case .good:
|
||||
self = .good(value, message)
|
||||
case .warning:
|
||||
self = .warning(value, message)
|
||||
case .error:
|
||||
self = .error(value, message)
|
||||
}
|
||||
}
|
||||
|
||||
public var key: Key {
|
||||
switch self {
|
||||
case .good:
|
||||
return .good
|
||||
case .warning:
|
||||
return .warning
|
||||
case .error:
|
||||
return .error
|
||||
}
|
||||
}
|
||||
|
||||
public var value: Value {
|
||||
switch self {
|
||||
case let .good(value, _):
|
||||
return value
|
||||
case let .warning(value, _):
|
||||
return value
|
||||
case let .error(value, _):
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public var message: String? {
|
||||
switch self {
|
||||
case let .good(_, message):
|
||||
return message
|
||||
case let .warning(_, message):
|
||||
return message
|
||||
case let .error(_, message):
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
public enum Key: String, Equatable, CaseIterable {
|
||||
case good, warning, error
|
||||
|
||||
public var title: String {
|
||||
self.rawValue.capitalized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FlaggedValue: Equatable where Value: Equatable { }
|
||||
|
||||
@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: FlaggedValue<Double> {
|
||||
let checkedResult = checkValue(wrappedValue)
|
||||
let key = checkedResult.key
|
||||
let message: String?
|
||||
|
||||
switch checkedResult {
|
||||
|
||||
case let .aboveMaximum(max):
|
||||
message = "Above maximum: \(doubleString(max))"
|
||||
|
||||
case let .belowMinimum(min):
|
||||
message = "Below minimum: \(doubleString(min))"
|
||||
|
||||
case .betweenRange(minimum: let minimum, maximum: let maximum):
|
||||
message = "Between: \(minimum) and \(maximum)"
|
||||
|
||||
case .betweenRatedAndMaximum(rated: let rated, maximum: let maximum):
|
||||
message = "Between rated: \(doubleString(rated)) and maximum: \(doubleString(maximum))"
|
||||
case let .good(goodMessage):
|
||||
message = goodMessage
|
||||
|
||||
case let .error(errorMessage):
|
||||
message = errorMessage
|
||||
}
|
||||
|
||||
return .init(wrappedValue, key: key, message: message)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
var key: FlaggedValue<Double>.Key {
|
||||
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 static func == (lhs: Flagged, rhs: Flagged) -> Bool {
|
||||
lhs.wrappedValue == rhs.wrappedValue
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember keyPath: KeyPath<FlaggedValue<Double>, 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: CoolingCapacity,
|
||||
ratings: RatedAirflowPerTon = .init(),
|
||||
goodMessage: Flagged.GoodMessageHandler? = nil
|
||||
) -> Self {
|
||||
.rated(RatedAirflowLimits(tons: tons, using: ratings), goodMessage: goodMessage)
|
||||
}
|
||||
|
||||
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)"
|
||||
}
|
||||
225
Sources/SharedModels/FlaggedEquipmentMeasurement.swift
Normal file
225
Sources/SharedModels/FlaggedEquipmentMeasurement.swift
Normal file
@@ -0,0 +1,225 @@
|
||||
import Foundation
|
||||
|
||||
// TODO: Needs updated for when forms are using `minmal` values.
|
||||
public struct FlaggedEquipmentMeasurement: Equatable {
|
||||
public var airflow: Flagged
|
||||
public var coilPressureDrop: Flagged
|
||||
public var externalStaticPressure: Flagged
|
||||
public var filterPressureDrop: Flagged
|
||||
public var returnPlenumPressure: Flagged
|
||||
public var supplyPlenumPressure: Flagged
|
||||
|
||||
public init(
|
||||
airflow: Flagged,
|
||||
coilPressureDrop: Flagged,
|
||||
externalStaticPressure: Flagged,
|
||||
filterPressureDrop: Flagged,
|
||||
returnPlenumPressure: Flagged,
|
||||
supplyPlenumPressure: Flagged
|
||||
) {
|
||||
self.airflow = airflow
|
||||
self.coilPressureDrop = coilPressureDrop
|
||||
self.externalStaticPressure = externalStaticPressure
|
||||
self.filterPressureDrop = filterPressureDrop
|
||||
self.returnPlenumPressure = returnPlenumPressure
|
||||
self.supplyPlenumPressure = supplyPlenumPressure
|
||||
}
|
||||
|
||||
public init(
|
||||
budgets: BudgetedPercentEnvelope,
|
||||
measurement: EquipmentMeasurement,
|
||||
ratedPressures: RatedStaticPressures,
|
||||
tons: CoolingCapacity?
|
||||
) {
|
||||
switch measurement {
|
||||
case let .airHandler(airHandler):
|
||||
self = .airHandler(
|
||||
budgets: budgets,
|
||||
measurement: airHandler,
|
||||
ratedPressures: ratedPressures,
|
||||
tons: tons
|
||||
)
|
||||
case let .furnaceAndCoil(furnaceAndCoil):
|
||||
self = .furnaceAndCoil(
|
||||
budgets: budgets,
|
||||
measurement: furnaceAndCoil,
|
||||
ratedPressures: ratedPressures,
|
||||
tons: tons
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static func airHandler(
|
||||
budgets: BudgetedPercentEnvelope,
|
||||
measurement: EquipmentMeasurement.AirHandler,
|
||||
ratedPressures: RatedStaticPressures,
|
||||
tons: CoolingCapacity?
|
||||
) -> Self {
|
||||
.init(
|
||||
airflow: checkAirflow(value: measurement.airflow, tons: tons),
|
||||
coilPressureDrop: .init(
|
||||
wrappedValue: (measurement.$postCoilPressure.positiveValue - measurement.$postFilterPressure.positiveValue) ?? 0,
|
||||
.result(.good())
|
||||
),
|
||||
externalStaticPressure: checkExternalStaticPressure(
|
||||
value: measurement.externalStaticPressure,
|
||||
ratedPressures: ratedPressures
|
||||
),
|
||||
filterPressureDrop: .init(
|
||||
value: measurement.$postFilterPressure.positiveValue - measurement.$returnPlenumPressure.positiveValue,
|
||||
budget: budgets.filterBudget,
|
||||
ratedPressures: ratedPressures
|
||||
),
|
||||
returnPlenumPressure: .init(
|
||||
value: measurement.$returnPlenumPressure.positiveValue,
|
||||
budget: budgets.returnPlenumBudget,
|
||||
ratedPressures: ratedPressures
|
||||
),
|
||||
supplyPlenumPressure: .init(
|
||||
value: measurement.$supplyPlenumPressure.positiveValue ?? 0,
|
||||
budget: budgets.supplyPlenumBudget,
|
||||
ratedPressures: ratedPressures
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public static func furnaceAndCoil(
|
||||
budgets: BudgetedPercentEnvelope,
|
||||
measurement: EquipmentMeasurement.FurnaceAndCoil,
|
||||
ratedPressures: RatedStaticPressures,
|
||||
tons: CoolingCapacity?
|
||||
) -> Self {
|
||||
.init(
|
||||
airflow: checkAirflow(value: measurement.airflow, tons: tons),
|
||||
coilPressureDrop: .init(
|
||||
value: measurement.$preCoilPressure.positiveValue - measurement.$supplyPlenumPressure.positiveValue,
|
||||
budget: budgets.coilBudget,
|
||||
ratedPressures: ratedPressures
|
||||
),
|
||||
externalStaticPressure: checkExternalStaticPressure(
|
||||
value: measurement.externalStaticPressure,
|
||||
ratedPressures: ratedPressures
|
||||
),
|
||||
filterPressureDrop: .init(
|
||||
value: measurement.$postFilterPressure.positiveValue - measurement.$returnPlenumPressure.positiveValue,
|
||||
budget: budgets.filterBudget,
|
||||
ratedPressures: ratedPressures,
|
||||
ignoreMinimum: true
|
||||
),
|
||||
returnPlenumPressure: .init(
|
||||
value: measurement.$returnPlenumPressure.positiveValue,
|
||||
budget: budgets.returnPlenumBudget,
|
||||
ratedPressures: ratedPressures
|
||||
),
|
||||
supplyPlenumPressure: .init(
|
||||
value: measurement.$supplyPlenumPressure.positiveValue,
|
||||
budget: budgets.supplyPlenumBudget,
|
||||
ratedPressures: ratedPressures
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Key
|
||||
extension FlaggedEquipmentMeasurement {
|
||||
// NOTE: These need to be kept in display order.
|
||||
public enum Key: Equatable, CaseIterable {
|
||||
case returnPlenum
|
||||
case filterDrop
|
||||
case coilDrop
|
||||
case supplyPlenum
|
||||
case staticPressure
|
||||
case airflow
|
||||
|
||||
public var title: String {
|
||||
switch self {
|
||||
case .returnPlenum:
|
||||
return "Return Plenum"
|
||||
case .filterDrop:
|
||||
return "Filter Pressure Drop"
|
||||
case .coilDrop:
|
||||
return "Coil Pressure Drop"
|
||||
case .supplyPlenum:
|
||||
return "Supply Plenum"
|
||||
case .staticPressure:
|
||||
return "External Static Pressure"
|
||||
case .airflow:
|
||||
return "System Airflow"
|
||||
}
|
||||
}
|
||||
|
||||
public var flaggedKeyPath: KeyPath<FlaggedEquipmentMeasurement, Flagged> {
|
||||
switch self {
|
||||
case .returnPlenum:
|
||||
return \.returnPlenumPressure
|
||||
case .filterDrop:
|
||||
return \.filterPressureDrop
|
||||
case .coilDrop:
|
||||
return \.coilPressureDrop
|
||||
case .supplyPlenum:
|
||||
return \.supplyPlenumPressure
|
||||
case .staticPressure:
|
||||
return \.externalStaticPressure
|
||||
case .airflow:
|
||||
return \.airflow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
fileprivate extension Flagged {
|
||||
|
||||
init(
|
||||
value: Double,
|
||||
budget: Percentage,
|
||||
ratedPressures: RatedStaticPressures,
|
||||
ignoreMinimum: Bool = false
|
||||
) {
|
||||
let minimum = ignoreMinimum ? 0 : ratedPressures.minimum * budget.fraction
|
||||
let maximum = ratedPressures.maximum * budget.fraction
|
||||
let rated = ratedPressures.rated * budget.fraction
|
||||
self.init(
|
||||
wrappedValue: value,
|
||||
.using(maximum: maximum, minimum: minimum, rated: rated)
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
value: Double?,
|
||||
budget: Percentage,
|
||||
ratedPressures: RatedStaticPressures,
|
||||
ignoreMinimum: Bool = false
|
||||
) {
|
||||
guard let value else {
|
||||
self = .error(message: "Value is not set.")
|
||||
return
|
||||
}
|
||||
self.init(
|
||||
value: value,
|
||||
budget: budget,
|
||||
ratedPressures: ratedPressures,
|
||||
ignoreMinimum: ignoreMinimum
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func checkExternalStaticPressure(
|
||||
value: Double,
|
||||
ratedPressures: RatedStaticPressures
|
||||
) -> Flagged {
|
||||
.init(
|
||||
wrappedValue: value,
|
||||
.rated(ratedPressures)
|
||||
)
|
||||
}
|
||||
|
||||
fileprivate func checkAirflow(
|
||||
value: Double?,
|
||||
tons: CoolingCapacity?
|
||||
) -> Flagged {
|
||||
guard let value, let tons else {
|
||||
return .init(wrappedValue: value ?? 0, .result(.good()))
|
||||
}
|
||||
return .init(wrappedValue: value, .airflow(tons: tons))
|
||||
}
|
||||
119
Sources/SharedModels/HelperTypes/Optional+Numeric.swift
Normal file
119
Sources/SharedModels/HelperTypes/Optional+Numeric.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
import Foundation
|
||||
|
||||
extension Optional: ExpressibleByIntegerLiteral where Wrapped: ExpressibleByIntegerLiteral {
|
||||
|
||||
public typealias IntegerLiteralType = Wrapped.IntegerLiteralType
|
||||
|
||||
public init(integerLiteral value: Wrapped.IntegerLiteralType) {
|
||||
self = .some(.init(integerLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: ExpressibleByFloatLiteral where Wrapped: ExpressibleByFloatLiteral {
|
||||
public typealias FloatLiteralType = Wrapped.FloatLiteralType
|
||||
|
||||
public init(floatLiteral value: Wrapped.FloatLiteralType) {
|
||||
self = .some(.init(floatLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: AdditiveArithmetic where Wrapped: AdditiveArithmetic {
|
||||
public static func - (lhs: Optional<Wrapped>, rhs: Optional<Wrapped>) -> Optional<Wrapped> {
|
||||
switch (lhs, rhs) {
|
||||
|
||||
case let (.some(lhs), .some(rhs)):
|
||||
return .some(lhs - rhs)
|
||||
|
||||
case let (.some(lhs), .none):
|
||||
return .some(lhs - .zero)
|
||||
|
||||
case let (.none, .some(rhs)):
|
||||
return .some(.zero - rhs)
|
||||
|
||||
case (.none, .none):
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
public static func + (lhs: Optional<Wrapped>, rhs: Optional<Wrapped>) -> Optional<Wrapped> {
|
||||
switch (lhs, rhs) {
|
||||
|
||||
case let (.some(lhs), .some(rhs)):
|
||||
return .some(lhs + rhs)
|
||||
|
||||
case let (.some(lhs), .none):
|
||||
return .some(lhs + .zero)
|
||||
|
||||
case let (.none, .some(rhs)):
|
||||
return .some(.zero + rhs)
|
||||
|
||||
case (.none, .none):
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
public static var zero: Optional<Wrapped> {
|
||||
.some(Wrapped.zero)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Optional: Numeric where Wrapped: Numeric {
|
||||
|
||||
public static func *= (lhs: inout Optional<Wrapped>, rhs: Optional<Wrapped>) {
|
||||
switch (lhs, rhs) {
|
||||
|
||||
case let (.some(lhsValue), .some(rhsValue)):
|
||||
lhs = lhsValue * rhsValue
|
||||
|
||||
case (.some, .none),
|
||||
(.none, .some),
|
||||
(.none, .none):
|
||||
lhs = .none
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public init?<T>(exactly source: T) where T : BinaryInteger {
|
||||
if let wrappedValue = Wrapped(exactly: source) {
|
||||
self = .some(wrappedValue)
|
||||
} else {
|
||||
self = .none
|
||||
}
|
||||
}
|
||||
|
||||
public var magnitude: Wrapped.Magnitude {
|
||||
switch self {
|
||||
case .none:
|
||||
return Self.zero!.magnitude
|
||||
case let .some(value):
|
||||
return value.magnitude
|
||||
}
|
||||
}
|
||||
|
||||
public static func * (lhs: Optional<Wrapped>, rhs: Optional<Wrapped>) -> Optional<Wrapped> {
|
||||
switch (lhs, rhs) {
|
||||
|
||||
case let (.some(lhs), .some(rhs)):
|
||||
return .some(lhs * rhs)
|
||||
|
||||
case (.some, .none),
|
||||
(.none, .some),
|
||||
(.none, .none):
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Magnitude = Wrapped.Magnitude
|
||||
}
|
||||
|
||||
extension Optional: Comparable where Wrapped: Comparable {
|
||||
public static func < (lhs: Optional<Wrapped>, rhs: Optional<Wrapped>) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.some(lhs), .some(rhs)):
|
||||
return lhs < rhs
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Sources/SharedModels/HelperTypes/Percentage.swift
Normal file
93
Sources/SharedModels/HelperTypes/Percentage.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
import Foundation
|
||||
|
||||
public struct Percentage: Equatable, RawRepresentable {
|
||||
|
||||
public var rawValue: Double
|
||||
|
||||
public init(rawValue: Double) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init(fraction: Double) {
|
||||
self.rawValue = fraction * 100
|
||||
}
|
||||
|
||||
public var fraction: Double {
|
||||
self.rawValue / 100
|
||||
}
|
||||
}
|
||||
|
||||
extension Percentage: ExpressibleByFloatLiteral {
|
||||
public typealias FloatLiteralType = Double
|
||||
|
||||
public init(floatLiteral value: Double) {
|
||||
self.init(rawValue: value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Percentage: ExpressibleByIntegerLiteral {
|
||||
public typealias IntegerLiteralType = Int
|
||||
|
||||
public init(integerLiteral value: Int) {
|
||||
self.init(rawValue: Double(value))
|
||||
}
|
||||
}
|
||||
|
||||
extension Percentage: Numeric {
|
||||
|
||||
public typealias Magnitude = Double.Magnitude
|
||||
|
||||
public init?<T>(exactly source: T) where T : BinaryInteger {
|
||||
self.init(rawValue: Double(source))
|
||||
}
|
||||
|
||||
public var magnitude: Double.Magnitude {
|
||||
rawValue.magnitude
|
||||
}
|
||||
|
||||
public static func * (lhs: Percentage, rhs: Percentage) -> Percentage {
|
||||
.init(rawValue: lhs.rawValue * rhs.rawValue)
|
||||
}
|
||||
|
||||
public static func + (lhs: Percentage, rhs: Percentage) -> Percentage {
|
||||
.init(rawValue: lhs.rawValue + rhs.rawValue)
|
||||
}
|
||||
|
||||
public static func *= (lhs: inout Percentage, rhs: Percentage) {
|
||||
lhs.rawValue *= rhs.rawValue
|
||||
}
|
||||
|
||||
public static func - (lhs: Percentage, rhs: Percentage) -> Percentage {
|
||||
.init(rawValue: lhs.rawValue - rhs.rawValue)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Percentage: Comparable {
|
||||
public static func < (lhs: Percentage, rhs: Percentage) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension Percentage: CustomStringConvertible {
|
||||
|
||||
static var formatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .percent
|
||||
return formatter
|
||||
}()
|
||||
|
||||
public var description: String {
|
||||
Self.formatter.string(for: fraction) ?? "\(String(format: "%g", rawValue))%"
|
||||
}
|
||||
}
|
||||
|
||||
postfix operator %
|
||||
|
||||
public postfix func %(value: Double) -> Percentage {
|
||||
.init(rawValue: value)
|
||||
}
|
||||
|
||||
public postfix func %(value: Int) -> Percentage {
|
||||
.init(rawValue: Double(value))
|
||||
}
|
||||
87
Sources/SharedModels/HelperTypes/Positive.swift
Normal file
87
Sources/SharedModels/HelperTypes/Positive.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
|
||||
@propertyWrapper
|
||||
public struct Positive<Value> where Value: Numeric, Value: Comparable {
|
||||
|
||||
public var wrappedValue: Value
|
||||
|
||||
public init(wrappedValue: Value) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public var positiveValue: Value {
|
||||
guard self.wrappedValue > .zero else {
|
||||
return wrappedValue * -1
|
||||
}
|
||||
return wrappedValue
|
||||
}
|
||||
|
||||
public var projectedValue: Self {
|
||||
get { self }
|
||||
set { self = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension Positive: Equatable where Value: Equatable { }
|
||||
|
||||
extension Positive: Comparable where Value: Comparable {
|
||||
public static func < (lhs: Positive<Value>, rhs: Positive<Value>) -> Bool {
|
||||
lhs.wrappedValue < rhs.wrappedValue
|
||||
}
|
||||
}
|
||||
|
||||
extension Positive: ExpressibleByIntegerLiteral where Value: ExpressibleByIntegerLiteral {
|
||||
public typealias IntegerLiteralType = Value.IntegerLiteralType
|
||||
|
||||
public init(integerLiteral value: Value.IntegerLiteralType) {
|
||||
self.init(wrappedValue: .init(integerLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension Positive: ExpressibleByFloatLiteral where Value: ExpressibleByFloatLiteral {
|
||||
public typealias FloatLiteralType = Value.FloatLiteralType
|
||||
|
||||
public init(floatLiteral value: Value.FloatLiteralType) {
|
||||
self.init(wrappedValue: .init(floatLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension Positive: AdditiveArithmetic where Value: AdditiveArithmetic {
|
||||
|
||||
public static func - (lhs: Positive<Value>, rhs: Positive<Value>) -> Positive<Value> {
|
||||
.init(wrappedValue: lhs.wrappedValue - rhs.wrappedValue)
|
||||
}
|
||||
|
||||
public static func + (lhs: Positive<Value>, rhs: Positive<Value>) -> Positive<Value> {
|
||||
.init(wrappedValue: lhs.wrappedValue + rhs.wrappedValue)
|
||||
}
|
||||
|
||||
public static var zero: Positive<Value> {
|
||||
.init(wrappedValue: Value.zero)
|
||||
}
|
||||
}
|
||||
|
||||
extension Positive: Numeric where Value: Numeric {
|
||||
public static func *= (lhs: inout Positive<Value>, rhs: Positive<Value>) {
|
||||
lhs.wrappedValue *= rhs.wrappedValue
|
||||
}
|
||||
|
||||
public init?<T>(exactly source: T) where T : BinaryInteger {
|
||||
guard let value = Value(exactly: source) else {
|
||||
return nil
|
||||
}
|
||||
self = .init(wrappedValue: value)
|
||||
}
|
||||
|
||||
public var magnitude: Value.Magnitude {
|
||||
wrappedValue.magnitude
|
||||
}
|
||||
|
||||
public static func * (lhs: Positive<Value>, rhs: Positive<Value>) -> Positive<Value> {
|
||||
.init(wrappedValue: lhs.wrappedValue * rhs.wrappedValue)
|
||||
}
|
||||
|
||||
public typealias Magnitude = Value.Magnitude
|
||||
|
||||
}
|
||||
|
||||
33
Sources/SharedModels/PlenumDimension.swift
Normal file
33
Sources/SharedModels/PlenumDimension.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
|
||||
public enum PlenumDimension: Equatable {
|
||||
case rectangular(width: Double, height: Double)
|
||||
case round(Double)
|
||||
|
||||
public var areaInSquareFeet: Double {
|
||||
switch self {
|
||||
|
||||
case .rectangular(width: let width, height: let height):
|
||||
return (width * height) / 144
|
||||
|
||||
case let .round(dimension):
|
||||
return pow((dimension / 12) / 2, 2) * Double.pi
|
||||
}
|
||||
}
|
||||
|
||||
public enum Key: String, Equatable, CaseIterable, CustomStringConvertible {
|
||||
|
||||
case rectangular, round
|
||||
|
||||
public var description: String { rawValue.capitalized }
|
||||
}
|
||||
|
||||
public var key: Key {
|
||||
switch self {
|
||||
case .rectangular:
|
||||
return .rectangular
|
||||
case .round:
|
||||
return .round
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Sources/SharedModels/RatedEnvelope.swift
Normal file
50
Sources/SharedModels/RatedEnvelope.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
import Foundation
|
||||
|
||||
public struct RatedEnvelope<Rating>: Equatable {
|
||||
public var maximum: Double
|
||||
public var minimum: Double
|
||||
public var rated: Double
|
||||
|
||||
public init(
|
||||
maximum: Double,
|
||||
minimum: Double,
|
||||
rated: Double
|
||||
) {
|
||||
self.maximum = maximum
|
||||
self.minimum = minimum
|
||||
self.rated = rated
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Namespaces
|
||||
public enum StaticPressure { }
|
||||
public enum AirflowPerTon { }
|
||||
public enum AirflowLimits { }
|
||||
|
||||
// MARK: - Static Pressures
|
||||
public typealias RatedStaticPressures = RatedEnvelope<StaticPressure>
|
||||
extension RatedStaticPressures {
|
||||
public init() {
|
||||
self.init(maximum: 0.8, minimum: 0.3, rated: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Airflow Per Ton
|
||||
public typealias RatedAirflowPerTon = RatedEnvelope<AirflowPerTon>
|
||||
extension RatedAirflowPerTon {
|
||||
public init() {
|
||||
self.init(maximum: 500, minimum: 350, rated: 400)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Airflow Limits
|
||||
public typealias RatedAirflowLimits = RatedEnvelope<AirflowLimits>
|
||||
extension RatedAirflowLimits {
|
||||
public init(tons: CoolingCapacity, using airflowPerTon: RatedAirflowPerTon = .init()) {
|
||||
self.init(
|
||||
maximum: airflowPerTon.maximum * tons.rawValue,
|
||||
minimum: airflowPerTon.minimum * tons.rawValue,
|
||||
rated: airflowPerTon.rated * tons.rawValue
|
||||
)
|
||||
}
|
||||
}
|
||||
18
Sources/SharedModels/Velocity.swift
Normal file
18
Sources/SharedModels/Velocity.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
|
||||
public struct Velocity: Equatable {
|
||||
public var airflow: Double
|
||||
public var dimension: PlenumDimension
|
||||
|
||||
public var value: Double {
|
||||
airflow / dimension.areaInSquareFeet
|
||||
}
|
||||
|
||||
public var flagged: Flagged { .velocity(self) }
|
||||
}
|
||||
|
||||
extension Flagged {
|
||||
public static func velocity(_ velocity: Velocity) -> Self {
|
||||
.init(wrappedValue: velocity.value, .velocity())
|
||||
}
|
||||
}
|
||||
94
Tests/EstimatedPressureTests/EstimatedPressureTests.swift
Normal file
94
Tests/EstimatedPressureTests/EstimatedPressureTests.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
import Dependencies
|
||||
import EstimatedPressureDependency
|
||||
import SharedModels
|
||||
import XCTest
|
||||
|
||||
final class PositiveNumericTests: XCTestCase {
|
||||
|
||||
override func invokeTest() {
|
||||
withDependencies {
|
||||
$0.estimatedPressuresClient = .liveValue
|
||||
} operation: {
|
||||
super.invokeTest()
|
||||
}
|
||||
}
|
||||
|
||||
func testEstimatedAirflow() async throws {
|
||||
@Dependency(\.estimatedPressuresClient.estimatedAirflow) var estimatedAirflow
|
||||
|
||||
let sut = try await estimatedAirflow(.init(
|
||||
existingAirflow: 1501,
|
||||
existingPressure: 0.874,
|
||||
targetPressure: 0.5
|
||||
))
|
||||
XCTAssertNotNil(sut)
|
||||
XCTAssertEqual(round(sut.positiveValue), 1135)
|
||||
}
|
||||
|
||||
func testEstimatedPressure() async throws {
|
||||
|
||||
@Dependency(\.estimatedPressuresClient.estimatedPressure) var estimatedPressure
|
||||
|
||||
let sut = try await estimatedPressure(.init(
|
||||
existingPressure: 0.3,
|
||||
existingAirflow: 1084,
|
||||
targetAirflow: 1050
|
||||
))
|
||||
XCTAssertEqual(round(sut.positiveValue * 1000) / 1000, 0.281)
|
||||
}
|
||||
|
||||
func testExternalStaticPressure() {
|
||||
var envelope = EquipmentMeasurement.furnaceAndCoil(
|
||||
.init(
|
||||
returnPlenumPressure: -0.1,
|
||||
postFilterPressure: -0.2, // here to test it makes positive.
|
||||
preCoilPressure: 0.4,
|
||||
supplyPlenumPressure: 0.1
|
||||
)
|
||||
)
|
||||
|
||||
var sut = envelope.externalStaticPressure
|
||||
XCTAssertEqual(round(sut * 10) / 10, 0.6)
|
||||
|
||||
|
||||
envelope = .airHandler(
|
||||
.init(
|
||||
returnPlenumPressure: -0.2,
|
||||
postFilterPressure: 0.3,
|
||||
postCoilPressure: 0.5,
|
||||
supplyPlenumPressure: 0.3)
|
||||
)
|
||||
|
||||
sut = envelope.externalStaticPressure
|
||||
XCTAssertEqual(round(sut * 10) / 10, 0.5)
|
||||
|
||||
}
|
||||
|
||||
func testExternalStaticPressureReturnsZero() {
|
||||
XCTAssertEqual(
|
||||
EquipmentMeasurement.airHandler(.init()).externalStaticPressure,
|
||||
0
|
||||
)
|
||||
XCTAssertEqual(
|
||||
EquipmentMeasurement.furnaceAndCoil(.init()).externalStaticPressure,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
func testValuesArePositiveOnly() {
|
||||
let sut = Positive<Double>(wrappedValue: -0.1)
|
||||
XCTAssertEqual(sut.positiveValue, 0.1)
|
||||
|
||||
let sut2 = Positive<Double?>(wrappedValue: -0.1)
|
||||
XCTAssertEqual(sut2.positiveValue, 0.1)
|
||||
}
|
||||
|
||||
func testReturnPlenumDimensionsAreaCalculation() {
|
||||
var sut = PlenumDimension.rectangular(width: 24, height: 12)
|
||||
XCTAssertEqual(sut.areaInSquareFeet, 2)
|
||||
|
||||
sut = .round(16)
|
||||
XCTAssertEqual(round(sut.areaInSquareFeet * 100) / 100, 1.4)
|
||||
|
||||
}
|
||||
}
|
||||
62
Tests/EstimatedPressureTests/PercentageTests.swift
Normal file
62
Tests/EstimatedPressureTests/PercentageTests.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import SharedModels
|
||||
import XCTest
|
||||
|
||||
final class PercentageTests: XCTestCase {
|
||||
|
||||
func testPostfixOperator() {
|
||||
var sut = 50%
|
||||
XCTAssertEqual(sut.rawValue, 50)
|
||||
|
||||
sut = 50.01%
|
||||
XCTAssertEqual(sut.rawValue, 50.01)
|
||||
}
|
||||
|
||||
func testFractionInitialization() {
|
||||
let sut = Percentage(fraction: 0.5)
|
||||
XCTAssertEqual(sut.rawValue, 50)
|
||||
XCTAssertEqual(sut.fraction, 0.5)
|
||||
}
|
||||
|
||||
func testExpressibleByFloatLiteral() {
|
||||
let sut: Percentage = 50.01
|
||||
XCTAssertEqual(sut.rawValue, 50.01)
|
||||
}
|
||||
|
||||
func testExpressibleByIntegerLiteral() {
|
||||
let sut: Percentage = 50
|
||||
XCTAssertEqual(sut.rawValue, 50)
|
||||
}
|
||||
|
||||
func testMultiplication() {
|
||||
var sut = 2% * 25%
|
||||
XCTAssertEqual(sut, 50%)
|
||||
|
||||
sut *= 2
|
||||
XCTAssertEqual(sut, 100%)
|
||||
}
|
||||
|
||||
func testAddition() {
|
||||
var sut = 35% + 15%
|
||||
XCTAssertEqual(sut, 50%)
|
||||
|
||||
sut += 25%
|
||||
XCTAssertEqual(sut, 75%)
|
||||
}
|
||||
|
||||
func testSubtraction() {
|
||||
let sut = 50% - 15%
|
||||
XCTAssertEqual(sut, 35%)
|
||||
}
|
||||
|
||||
func testComparable() {
|
||||
let sut1 = 50%
|
||||
let sut2 = 35%
|
||||
|
||||
XCTAssert(sut2 < sut1)
|
||||
}
|
||||
|
||||
func testCustomStringConvertible() {
|
||||
let sut = 50%
|
||||
XCTAssertEqual(sut.description, "50%")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user