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