From 0aabd612b2c1eebd91dd6d260b454d663c35c45e Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 19 Dec 2025 11:57:21 -0500 Subject: [PATCH] feat: Starts manual-d client and adds friction rate calculation and tests. --- Package.resolved | 69 +++++++++++++++++++ Package.swift | 16 +++++ Sources/ManualDClient/Helpers.swift | 6 ++ Sources/ManualDClient/Live.swift | 18 +++++ Sources/ManualDClient/ManualDClient.swift | 50 ++++++++++++++ Sources/ManualDClient/ManualDError.swift | 5 ++ .../ManualDCore/ComponentPressureLosses.swift | 16 ++++- Sources/ManualDCore/CoolingLoad.swift | 2 +- .../ManualDCore/EffectiveLengthGroup.swift | 8 +-- Sources/ManualDCore/Room.swift | 2 +- .../ManualDClientTests.swift | 45 ++++++++++++ 11 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 Package.resolved create mode 100644 Sources/ManualDClient/Helpers.swift create mode 100644 Sources/ManualDClient/Live.swift create mode 100644 Sources/ManualDClient/ManualDClient.swift create mode 100644 Sources/ManualDClient/ManualDError.swift create mode 100644 Tests/ManualDClientTests/ManualDClientTests.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..9f6fd04 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,69 @@ +{ + "originHash" : "426df0aee89a834f20c1c804ecbfbed0bc19ef629c2a1fd2e6260702b97b6f31", + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "fd16d76fd8b9a976d88bfb6cacc05ca8d19c91b6", + "version" : "1.1.0" + } + }, + { + "identity" : "opencombine", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenCombine/OpenCombine.git", + "state" : { + "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", + "version" : "0.14.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "5a3825302b1a0d744183200915a47b508c828e6f", + "version" : "1.3.2" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "a10f9feeb214bc72b5337b6ef6d5a029360db4cc", + "version" : "1.10.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "4799286537280063c85a32f09884cfbca301b1a1", + "version" : "602.0.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "31073495cae9caf243c440eac94b3ab067e3d7bc", + "version" : "1.8.0" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 17a1719..549d483 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,10 @@ let package = Package( products: [ .library(name: "swift-manual-d", targets: ["swift-manual-d"]), .library(name: "ManualDCore", targets: ["ManualDCore"]), + .library(name: "ManualDClient", targets: ["ManualDClient"]), + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0") ], targets: [ .target( @@ -15,6 +19,18 @@ let package = Package( .target( name: "ManualDCore" ), + .target( + name: "ManualDClient", + dependencies: [ + "ManualDCore", + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), + ] + ), + .testTarget( + name: "ManualDClientTests", + dependencies: ["ManualDClient"] + ), .testTarget( name: "swift-manual-dTests", dependencies: ["swift-manual-d"] diff --git a/Sources/ManualDClient/Helpers.swift b/Sources/ManualDClient/Helpers.swift new file mode 100644 index 0000000..0cacd78 --- /dev/null +++ b/Sources/ManualDClient/Helpers.swift @@ -0,0 +1,6 @@ +import Foundation +import ManualDCore + +extension ComponentPressureLosses { + var totalLosses: Double { values.reduce(0) { $0 + $1 } } +} diff --git a/Sources/ManualDClient/Live.swift b/Sources/ManualDClient/Live.swift new file mode 100644 index 0000000..c070bd0 --- /dev/null +++ b/Sources/ManualDClient/Live.swift @@ -0,0 +1,18 @@ +import Dependencies +import ManualDCore + +extension ManualDClient: DependencyKey { + public static let liveValue: Self = .init( + frictionRate: { request in + // Ensure the total effective length is greater than 0. + guard request.totalEffectiveLength > 0 else { + throw ManualDError(message: "Total Effective Length should be greater than 0.") + } + + let totalComponentLosses = request.componentPressureLosses.totalLosses + let availableStaticPressure = request.externalStaticPressure - totalComponentLosses + let frictionRate = availableStaticPressure * 100.0 / Double(request.totalEffectiveLength) + return .init(availableStaticPressure: availableStaticPressure, frictionRate: frictionRate) + } + ) +} diff --git a/Sources/ManualDClient/ManualDClient.swift b/Sources/ManualDClient/ManualDClient.swift new file mode 100644 index 0000000..3c6ddf3 --- /dev/null +++ b/Sources/ManualDClient/ManualDClient.swift @@ -0,0 +1,50 @@ +import Dependencies +import DependenciesMacros +import ManualDCore + +@DependencyClient +public struct ManualDClient: Sendable { + public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRateResponse +} + +extension ManualDClient: TestDependencyKey { + public static let testValue = Self() +} + +extension DependencyValues { + public var manualD: ManualDClient { + get { self[ManualDClient.self] } + set { self[ManualDClient.self] = newValue } + } +} + +// MARK: - Friction Rate +extension ManualDClient { + public struct FrictionRateRequest: Codable, Equatable, Sendable { + + public let externalStaticPressure: Double + public let componentPressureLosses: ComponentPressureLosses + public let totalEffectiveLength: Int + + public init( + externalStaticPressure: Double, + componentPressureLosses: ComponentPressureLosses, + totalEffectiveLength: Int + ) { + self.externalStaticPressure = externalStaticPressure + self.componentPressureLosses = componentPressureLosses + self.totalEffectiveLength = totalEffectiveLength + } + } + + public struct FrictionRateResponse: Codable, Equatable, Sendable { + + public let availableStaticPressure: Double + public let frictionRate: Double + + public init(availableStaticPressure: Double, frictionRate: Double) { + self.availableStaticPressure = availableStaticPressure + self.frictionRate = frictionRate + } + } +} diff --git a/Sources/ManualDClient/ManualDError.swift b/Sources/ManualDClient/ManualDError.swift new file mode 100644 index 0000000..6b932bf --- /dev/null +++ b/Sources/ManualDClient/ManualDError.swift @@ -0,0 +1,5 @@ +import Foundation + +public struct ManualDError: Error { + public let message: String +} diff --git a/Sources/ManualDCore/ComponentPressureLosses.swift b/Sources/ManualDCore/ComponentPressureLosses.swift index d796d99..9128b24 100644 --- a/Sources/ManualDCore/ComponentPressureLosses.swift +++ b/Sources/ManualDCore/ComponentPressureLosses.swift @@ -2,6 +2,16 @@ import Foundation public typealias ComponentPressureLosses = [String: Double] -extension ComponentPressureLosses { - public var totalLosses: Double { values.reduce(0) { $0 + $1 } } -} +#if DEBUG + extension ComponentPressureLosses { + public static var mock: Self { + [ + "evaporator-coil": 0.2, + "filter": 0.1, + "supply-outlet": 0.03, + "return-grille": 0.03, + "balancing-damper": 0.03, + ] + } + } +#endif diff --git a/Sources/ManualDCore/CoolingLoad.swift b/Sources/ManualDCore/CoolingLoad.swift index f018e9e..02b95d7 100644 --- a/Sources/ManualDCore/CoolingLoad.swift +++ b/Sources/ManualDCore/CoolingLoad.swift @@ -1,6 +1,6 @@ import Foundation -public struct CoolingLoad: Codable, Equatable { +public struct CoolingLoad: Codable, Equatable, Sendable { public let total: Double public let sensible: Double public var latent: Double { total - sensible } diff --git a/Sources/ManualDCore/EffectiveLengthGroup.swift b/Sources/ManualDCore/EffectiveLengthGroup.swift index f7f5935..a621ac8 100644 --- a/Sources/ManualDCore/EffectiveLengthGroup.swift +++ b/Sources/ManualDCore/EffectiveLengthGroup.swift @@ -2,7 +2,7 @@ import Foundation // TODO: Add other description / label for items that have same group & letter, but // different effective length. -public struct EffectiveLengthGroup: Codable, Equatable { +public struct EffectiveLengthGroup: Codable, Equatable, Sendable { public let group: Int public let letter: String public let effectiveLength: Int @@ -24,7 +24,7 @@ public struct EffectiveLengthGroup: Codable, Equatable { extension EffectiveLengthGroup { - public enum Category: String, Codable, Equatable { + public enum Category: String, Codable, Equatable, Sendable { case any case supply case `return` @@ -32,7 +32,7 @@ extension EffectiveLengthGroup { } -public let effectiveLengthsLookup: [String: EffectiveLengthGroup] { +public let effectiveLengthsLookup: [String: EffectiveLengthGroup] = { [ "1a": .init(group: 1, letter: "a", effectiveLength: 35, category: .supply), "1b": .init(group: 1, letter: "b", effectiveLength: 10, category: .supply), @@ -625,4 +625,4 @@ public let effectiveLengthsLookup: [String: EffectiveLengthGroup] { "12u": .init(group: 12, letter: "u", effectiveLength: 25, category: .any), "12v": .init(group: 12, letter: "v", effectiveLength: 30, category: .any), ] -} +}() diff --git a/Sources/ManualDCore/Room.swift b/Sources/ManualDCore/Room.swift index 2dd6b2c..201f46c 100644 --- a/Sources/ManualDCore/Room.swift +++ b/Sources/ManualDCore/Room.swift @@ -1,6 +1,6 @@ import Foundation -public struct Room: Codable, Equatable { +public struct Room: Codable, Equatable, Sendable { public let name: String public let heatingLoad: Double public let coolingLoad: CoolingLoad diff --git a/Tests/ManualDClientTests/ManualDClientTests.swift b/Tests/ManualDClientTests/ManualDClientTests.swift new file mode 100644 index 0000000..dc9db5a --- /dev/null +++ b/Tests/ManualDClientTests/ManualDClientTests.swift @@ -0,0 +1,45 @@ +import Dependencies +import Foundation +import ManualDClient +import ManualDCore +import Testing + +@Suite("ManualDClient Tests") +struct ManualDClientTests { + + var numberFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.minimumFractionDigits = 2 + formatter.maximumFractionDigits = 2 + formatter.roundingMode = .halfUp + return formatter + } + + @Test + func frictionRate() async throws { + let manualD = ManualDClient.liveValue + let response = try await manualD.frictionRate( + .init( + externalStaticPressure: 0.5, + componentPressureLosses: .mock, + totalEffectiveLength: 185 + ) + ) + #expect(numberFormatter.string(for: response.availableStaticPressure) == "0.11") + #expect(numberFormatter.string(for: response.frictionRate) == "0.06") + } + + @Test + func frictionRateFails() async throws { + await #expect(throws: ManualDError.self) { + let manualD = ManualDClient.liveValue + _ = try await manualD.frictionRate( + .init( + externalStaticPressure: 0.5, + componentPressureLosses: .mock, + totalEffectiveLength: 0 + ) + ) + } + } +}