From 861ff3bfd647fb9ee0da5b4fabc6f33964b41729 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Sun, 21 Dec 2025 14:40:36 -0500 Subject: [PATCH] feat: Adds duct sizing calculations. --- Sources/ManualDClient/Helpers.swift | 57 +++++++++++++++++++ Sources/ManualDClient/Live.swift | 15 +++++ Sources/ManualDClient/ManualDClient.swift | 37 ++++++++++++ .../ManualDClientTests.swift | 11 ++++ 4 files changed, 120 insertions(+) diff --git a/Sources/ManualDClient/Helpers.swift b/Sources/ManualDClient/Helpers.swift index 51f1c7d..bd2a95d 100644 --- a/Sources/ManualDClient/Helpers.swift +++ b/Sources/ManualDClient/Helpers.swift @@ -10,3 +10,60 @@ extension Array where Element == EffectiveLengthGroup { reduce(0) { $0 + $1.effectiveLength } } } + +func roundSize(_ size: Double) throws -> Int { + guard size > 0 else { + throw ManualDError(message: "Size should be greater than 0.") + } + guard size <= 24 else { + throw ManualDError(message: "Size should be less than 24.") + } + + switch size { + case 0..<4: + return 4 + case 4..<5: + return 5 + case 5..<6: + return 6 + case 6..<7: + return 7 + case 7..<8: + return 8 + case 8..<9: + return 9 + case 9..<10: + return 10 + case 10..<12: + return 12 + case 12..<14: + return 14 + case 14..<16: + return 16 + case 16..<18: + return 18 + case 18..<20: + return 20 + case 20..<22: + return 2 + case 22..<24: + return 24 + default: + throw ManualDError(message: "Size '\(size)' not in range.") + + } +} + +func velocity(cfm: Int, roundSize: Int) -> Int { + let cfm = Double(cfm) + let roundSize = Double(roundSize) + let velocity = cfm / (pow(roundSize / 24, 2) * 3.14) + return Int(round(velocity)) +} + +func flexSize(_ request: ManualDClient.DuctSizeRequest) throws -> Int { + let cfm = pow(Double(request.designCFM), 0.4) + let fr = pow(request.frictionRate / 1.76, 0.2) + let size = 0.55 * (cfm / fr) + return try roundSize(size) +} diff --git a/Sources/ManualDClient/Live.swift b/Sources/ManualDClient/Live.swift index 81c089a..c46a066 100644 --- a/Sources/ManualDClient/Live.swift +++ b/Sources/ManualDClient/Live.swift @@ -4,6 +4,21 @@ import ManualDCore extension ManualDClient: DependencyKey { public static let liveValue: Self = .init( + ductSize: { request in + guard request.designCFM > 0 else { + throw ManualDError(message: "Design CFM should be greater than 0.") + } + let fr = pow(request.frictionRate, 0.5) + let ductulatorSize = pow(Double(request.designCFM) / (3.12 * fr), 0.38) + let finalSize = try roundSize(ductulatorSize) + let flexSize = try flexSize(request) + return .init( + ductulatorSize: ductulatorSize, + finalSize: finalSize, + flexSize: flexSize, + velocity: velocity(cfm: request.designCFM, roundSize: finalSize) + ) + }, frictionRate: { request in // Ensure the total effective length is greater than 0. guard request.totalEffectiveLength > 0 else { diff --git a/Sources/ManualDClient/ManualDClient.swift b/Sources/ManualDClient/ManualDClient.swift index 3ec5106..ed31aee 100644 --- a/Sources/ManualDClient/ManualDClient.swift +++ b/Sources/ManualDClient/ManualDClient.swift @@ -4,6 +4,7 @@ import ManualDCore @DependencyClient public struct ManualDClient: Sendable { + public var ductSize: @Sendable (DuctSizeRequest) async throws -> DuctSizeResponse public var frictionRate: @Sendable (FrictionRateRequest) async throws -> FrictionRateResponse public var totalEffectiveLength: @Sendable (TotalEffectiveLengthRequest) async throws -> Int public var equivalentRectangularDuct: @@ -21,6 +22,42 @@ extension DependencyValues { } } +// MARK: Duct Size +extension ManualDClient { + public struct DuctSizeRequest: Codable, Equatable, Sendable { + public let designCFM: Int + public let frictionRate: Double + + public init( + designCFM: Int, + frictionRate: Double + ) { + self.designCFM = designCFM + self.frictionRate = frictionRate + } + } + + public struct DuctSizeResponse: Codable, Equatable, Sendable { + + public let ductulatorSize: Double + public let finalSize: Int + public let flexSize: Int + public let velocity: Int + + public init( + ductulatorSize: Double, + finalSize: Int, + flexSize: Int, + velocity: Int + ) { + self.ductulatorSize = ductulatorSize + self.finalSize = finalSize + self.flexSize = flexSize + self.velocity = velocity + } + } +} + // MARK: - Friction Rate extension ManualDClient { public struct FrictionRateRequest: Codable, Equatable, Sendable { diff --git a/Tests/ManualDClientTests/ManualDClientTests.swift b/Tests/ManualDClientTests/ManualDClientTests.swift index 402281b..8af9ae5 100644 --- a/Tests/ManualDClientTests/ManualDClientTests.swift +++ b/Tests/ManualDClientTests/ManualDClientTests.swift @@ -22,6 +22,17 @@ struct ManualDClientTests { return formatter } + @Test + func ductSize() async throws { + let response = try await manualD.ductSize( + .init(designCFM: 88, frictionRate: 0.06) + ) + #expect(numberFormatter.string(for: response.ductulatorSize) == "6.07") + #expect(response.finalSize == 7) + #expect(response.flexSize == 7) + #expect(response.velocity == 329) + } + @Test func frictionRate() async throws { let response = try await manualD.frictionRate(