feat: Initial commit

This commit is contained in:
2024-05-24 16:44:41 -04:00
commit c1741de3f9
19 changed files with 1560 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved

View File

@@ -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
View 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"
]
),
]
)

View 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
}

View 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%
)
}
}
}

View 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 }
}

View 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
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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)"
}

View 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))
}

View 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
}
}
}

View 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))
}

View 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
}

View 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
}
}
}

View 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
)
}
}

View 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())
}
}

View 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)
}
}

View 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%")
}
}