feat: Starts manual-d client and adds friction rate calculation and tests.

This commit is contained in:
2025-12-19 11:57:21 -05:00
parent 5b67792051
commit 0aabd612b2
11 changed files with 228 additions and 9 deletions

69
Package.resolved Normal file
View File

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

View File

@@ -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"]

View File

@@ -0,0 +1,6 @@
import Foundation
import ManualDCore
extension ComponentPressureLosses {
var totalLosses: Double { values.reduce(0) { $0 + $1 } }
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
import Foundation
public struct ManualDError: Error {
public let message: String
}

View File

@@ -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

View File

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

View File

@@ -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),
]
}
}()

View File

@@ -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

View File

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