This repository has been archived on 2026-02-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2026-02-10 12:07:44 -05:00

431 lines
12 KiB
Swift

import Dependencies
import Foundation
import Tagged
/// Represents a room in a project.
///
/// This contains data such as the heating and cooling load for the
/// room, the number of registers in the room, and any rectangular
/// duct size calculations stored for the room.
public struct Room: Codable, Equatable, Identifiable, Sendable {
/// The unique id of the room.
public let id: UUID
/// The project this room is associated with.
public let projectID: Project.ID
/// A unique name for the room in the project.
public let name: String
/// The level of the home the room is on.
public let level: Level?
/// The heating load required for the room (from Manual-J).
public let heatingLoad: Double
/// The cooling load required for the room (from Manual-J).
public let coolingLoad: CoolingLoad
/// The number of registers for the room.
public let registerCount: Int
/// An optional room that the airflow is delegated to.
public let delegatedTo: Room.ID?
/// The rectangular duct size calculations for a room.
///
/// **NOTE:** These are optionally set after the round sizes have been calculate
/// for a room.
public let rectangularSizes: [RectangularSize]?
/// When the room was created in the database.
public let createdAt: Date
/// When the room was updated in the database.
public let updatedAt: Date
public init(
id: UUID,
projectID: Project.ID,
name: String,
level: Level? = nil,
heatingLoad: Double,
coolingLoad: CoolingLoad,
registerCount: Int = 1,
delegatedTo: Room.ID? = nil,
rectangularSizes: [RectangularSize]? = nil,
createdAt: Date,
updatedAt: Date
) {
self.id = id
self.projectID = projectID
self.name = name
self.level = level
self.heatingLoad = heatingLoad
self.coolingLoad = coolingLoad
self.registerCount = registerCount
self.delegatedTo = delegatedTo
self.rectangularSizes = rectangularSizes
self.createdAt = createdAt
self.updatedAt = updatedAt
}
/// Represents the cooling load of a room.
///
/// Generally only one of the values is provided by a Manual-J room x room
/// calculation.
///
public struct CoolingLoad: Codable, Equatable, Sendable {
public let total: Double?
public let sensible: Double?
public init(total: Double? = nil, sensible: Double? = nil) {
self.total = total
self.sensible = sensible
}
/// Calculates the cooling load based on the shr.
///
/// Generally Manual-J room x room loads provide either the total load or the
/// sensible load, so this allows us to calculate whichever is not provided.
public func ensured(shr: Double) throws -> (total: Double, sensible: Double) {
switch (total, sensible) {
case (.none, .none):
throw CoolingLoadError("Both the total and sensible loads are nil.")
case (.some(let total), .some(let sensible)):
return (total, sensible)
case (.some(let total), .none):
return (total, total * shr)
case (.none, .some(let sensible)):
return (sensible / shr, sensible)
}
}
}
public enum LevelTag {}
public typealias Level = Tagged<LevelTag, Int>
}
extension Room {
/// Represents the data required to create a new room for a project.
public struct Create: Codable, Equatable, Sendable {
/// A unique name for the room in the project.
public let name: String
/// An optional level of the home the room is on.
public let level: Room.Level?
/// The heating load required for the room (from Manual-J).
public let heatingLoad: Double
/// The total cooling load required for the room (from Manual-J).
public let coolingTotal: Double?
/// An optional sensible cooling load for the room.
public let coolingSensible: Double?
/// The number of registers for the room.
public let registerCount: Int
/// An optional room that this room delegates it's airflow to.
public let delegatedTo: Room.ID?
public var coolingLoad: Room.CoolingLoad {
.init(total: coolingTotal, sensible: coolingSensible)
}
public init(
name: String,
level: Room.Level? = nil,
heatingLoad: Double,
coolingTotal: Double? = nil,
coolingSensible: Double? = nil,
registerCount: Int = 1,
delegatedTo: Room.ID? = nil
) {
self.name = name
self.level = level
self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
self.registerCount = registerCount
self.delegatedTo = delegatedTo
}
}
public struct CSV: Equatable, Sendable {
public let file: Data
public init(file: Data) {
self.file = file
}
/// Represents a row in a CSV file.
///
/// This is similar to ``Room.Create``, but since the rooms are not yet
/// created, delegating to another room is done via the room's name
/// instead of id.
///
public struct Row: Codable, Equatable, Sendable {
/// A unique name for the room in the project.
public let name: String
/// An optional level of the home the room is on.
public let level: Room.Level?
/// The heating load required for the room (from Manual-J).
public let heatingLoad: Double
/// The total cooling load required for the room (from Manual-J).
public let coolingTotal: Double?
/// An optional sensible cooling load for the room.
public let coolingSensible: Double?
/// The number of registers for the room.
public let registerCount: Int
/// An optional room that this room delegates it's airflow to.
public let delegatedToName: String?
public init(
name: String,
level: Room.Level? = nil,
heatingLoad: Double,
coolingTotal: Double? = nil,
coolingSensible: Double? = nil,
registerCount: Int,
delegatedToName: String? = nil
) {
self.name = name
self.level = level
self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
self.registerCount = registerCount
// Treat empty strings as nil, as they are often empty
// when left blank in a CSV file.
self.delegatedToName = delegatedToName == "" ? nil : delegatedToName
}
}
}
/// Represents a rectangular size calculation that is stored in the
/// database for a given room.
///
/// These are done after the round duct sizes have been calculated and
/// can be used to calculate the equivalent rectangular size for a given run.
public struct RectangularSize: Codable, Equatable, Identifiable, Sendable {
/// The unique id of the rectangular size.
public let id: UUID
/// The register the rectangular size is associated with.
public let register: Int?
/// The height of the rectangular size, the width gets calculated.
public let height: Int
public init(
id: UUID = .init(),
register: Int? = nil,
height: Int,
) {
self.id = id
self.register = register
self.height = height
}
}
/// Represents field that can be updated on a room after it's been created in the database.
///
/// Onlly fields that are supplied get updated.
public struct Update: Codable, Equatable, Sendable {
/// A unique name for the room in the project.
public let name: String?
/// An optional level of the home the room is on.
public let level: Room.Level?
/// The heating load required for the room (from Manual-J).
public let heatingLoad: Double?
/// The total cooling load required for the room (from Manual-J).
public let coolingTotal: Double?
/// An optional sensible cooling load for the room.
public let coolingSensible: Double?
/// The number of registers for the room.
public let registerCount: Int?
/// The rectangular duct size calculations for a room.
public let rectangularSizes: [RectangularSize]?
public var coolingLoad: CoolingLoad? {
guard coolingTotal != nil || coolingSensible != nil else {
return nil
}
return .init(total: coolingTotal, sensible: coolingSensible)
}
public init(
name: String? = nil,
level: Room.Level? = nil,
heatingLoad: Double? = nil,
coolingTotal: Double? = nil,
coolingSensible: Double? = nil,
registerCount: Int? = nil
) {
self.name = name
self.level = level
self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal
self.coolingSensible = coolingSensible
self.registerCount = registerCount
self.rectangularSizes = nil
}
public init(
rectangularSizes: [RectangularSize]
) {
self.name = nil
self.level = nil
self.heatingLoad = nil
self.coolingTotal = nil
self.coolingSensible = nil
self.registerCount = nil
self.rectangularSizes = rectangularSizes
}
}
}
extension Array where Element == Room {
/// The sum of heating loads for an array of rooms.
public var totalHeatingLoad: Double {
reduce(into: 0) { $0 += $1.heatingLoad }
}
/// The sum of total cooling loads for an array of rooms.
public func totalCoolingLoad(shr: Double) throws -> Double {
try reduce(into: 0) { $0 += try $1.coolingLoad.ensured(shr: shr).total }
}
/// The sum of sensible cooling loads for an array of rooms.
///
/// - Parameters:
/// - shr: The project wide sensible heat ratio.
public func totalCoolingSensible(shr: Double) throws -> Double {
try reduce(into: 0) {
// let sensible = $1.coolingSensible ?? ($1.coolingTotal * shr)
$0 += try $1.coolingLoad.ensured(shr: shr).sensible
}
}
}
public struct CoolingLoadError: Error, Equatable, Sendable {
public let reason: String
public init(_ reason: String) {
self.reason = reason
}
}
extension Room.Level {
/// The label for the level, i.e. 'Basement' or 'Level-1', etc.
public var label: String {
if rawValue <= 0 {
return "Basement"
}
return "Level-\(rawValue)"
}
}
#if DEBUG
extension Room {
public static func mock(projectID: Project.ID) -> [Self] {
@Dependency(\.uuid) var uuid
@Dependency(\.date.now) var now
return [
.init(
id: uuid(),
projectID: projectID,
name: "Bed-1",
heatingLoad: 3913,
coolingLoad: .init(total: 2472),
// coolingSensible: nil,
registerCount: 1,
rectangularSizes: nil,
createdAt: now,
updatedAt: now
),
.init(
id: uuid(),
projectID: projectID,
name: "Entry",
heatingLoad: 8284,
coolingLoad: .init(total: 2916),
// coolingSensible: nil,
registerCount: 2,
rectangularSizes: nil,
createdAt: now,
updatedAt: now
),
.init(
id: uuid(),
projectID: projectID,
name: "Family Room",
heatingLoad: 9785,
coolingLoad: .init(total: 7446),
// coolingSensible: nil,
registerCount: 3,
rectangularSizes: nil,
createdAt: now,
updatedAt: now
),
.init(
id: uuid(),
projectID: projectID,
name: "Kitchen",
heatingLoad: 4518,
coolingLoad: .init(total: 5096),
// coolingSensible: nil,
registerCount: 2,
rectangularSizes: nil,
createdAt: now,
updatedAt: now
),
.init(
id: uuid(),
projectID: projectID,
name: "Living Room",
heatingLoad: 7553,
coolingLoad: .init(total: 6829),
// coolingSensible: nil,
registerCount: 2,
rectangularSizes: nil,
createdAt: now,
updatedAt: now
),
.init(
id: uuid(),
projectID: projectID,
name: "Master",
heatingLoad: 8202,
coolingLoad: .init(total: 2076),
// coolingSensible: nil,
registerCount: 2,
rectangularSizes: nil,
createdAt: now,
updatedAt: now
),
]
}
}
#endif