- Fix AWG 0/00/000/0000 bug (all resolved to 0 in Swift) using negative int convention (-1 through -4) with formatAWG() for 1/0–4/0 display - Add 7.5A fuse size and change fuse type from Int to Double - Add alternator power source type with distinct bolt.car.fill icon - Migrate all NSLocalizedString calls to String(localized:defaultValue:) - Update translations for runtime subtitle (ES/FR/NL: current→maximum), usable capacity footer text, and NL override wording - Store length always in meters, convert at display time in CalculatorView - Add preview-friendly inits for ComponentLibraryView and LoadsView - Expand test coverage for calculations, fuses, AWG, and edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
147 lines
4.7 KiB
Swift
147 lines
4.7 KiB
Swift
//
|
|
// ElectricalCalculations.swift
|
|
// Cable
|
|
//
|
|
// Created by GPT on request.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
struct ElectricalCalculations {
|
|
private static let maxVoltageDropFraction = 0.05
|
|
private static let copperResistivity = 0.017 // Ω⋅mm²/m
|
|
private static let feetToMeters = 0.3048
|
|
|
|
private static let standardMetricCrossSections: [Double] = [
|
|
0.75, 1.0, 1.5, 2.5, 4.0, 6.0, 10.0, 16.0, 25.0, 35.0, 50.0, 70.0, 95.0, 120.0,
|
|
150.0, 185.0, 240.0, 300.0, 400.0, 500.0, 630.0,
|
|
]
|
|
|
|
// Convention: 1/0 = -1, 2/0 = -2, 3/0 = -3, 4/0 = -4
|
|
private static let standardAWG: [Int] = [20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 1, -1, -2, -3, -4]
|
|
private static let awgCrossSections: [Double] = [
|
|
0.519, 0.823, 1.31, 2.08, 3.31, 5.26, 8.37, 13.3, 21.2, 33.6, 42.4, 53.5, 67.4, 85.0, 107.0,
|
|
]
|
|
|
|
private static let standardFuses: [Double] = [
|
|
1, 2, 3, 5, 7.5, 10, 15, 20, 25, 30, 35, 40, 50,
|
|
60, 70, 80, 100, 125, 150, 175, 200, 225, 250,
|
|
300, 350, 400, 450, 500, 600, 700, 800,
|
|
]
|
|
|
|
/// Length must always be in meters. unitSystem controls the output format (mm² vs AWG).
|
|
static func recommendedCrossSection(
|
|
length: Double,
|
|
current: Double,
|
|
voltage: Double,
|
|
unitSystem: UnitSystem
|
|
) -> Double {
|
|
let lengthInMeters = length
|
|
let maxVoltageDrop = voltage * maxVoltageDropFraction
|
|
let minimumCrossSection = guardAgainstZero(maxVoltageDrop) {
|
|
(2 * current * lengthInMeters * copperResistivity) / maxVoltageDrop
|
|
}
|
|
|
|
if unitSystem == .imperial {
|
|
for (index, crossSection) in awgCrossSections.enumerated() where crossSection >= minimumCrossSection {
|
|
return Double(standardAWG[index])
|
|
}
|
|
return Double(standardAWG.last ?? 0)
|
|
} else {
|
|
return standardMetricCrossSections.first { $0 >= max(standardMetricCrossSections.first ?? 0.75, minimumCrossSection) }
|
|
?? standardMetricCrossSections.last ?? 0.75
|
|
}
|
|
}
|
|
|
|
static func voltageDrop(
|
|
length: Double,
|
|
current: Double,
|
|
voltage: Double,
|
|
unitSystem: UnitSystem,
|
|
crossSection: Double? = nil
|
|
) -> Double {
|
|
let selectedCrossSection = crossSection ?? recommendedCrossSection(
|
|
length: length,
|
|
current: current,
|
|
voltage: voltage,
|
|
unitSystem: unitSystem
|
|
)
|
|
|
|
let lengthInMeters = length
|
|
let crossSectionMM2: Double
|
|
if unitSystem == .metric {
|
|
crossSectionMM2 = selectedCrossSection
|
|
} else {
|
|
crossSectionMM2 = crossSectionFromAWG(selectedCrossSection)
|
|
}
|
|
|
|
guard crossSectionMM2 > 0 else { return 0 }
|
|
return (2 * current * lengthInMeters * copperResistivity) / crossSectionMM2
|
|
}
|
|
|
|
static func voltageDropPercentage(
|
|
length: Double,
|
|
current: Double,
|
|
voltage: Double,
|
|
unitSystem: UnitSystem,
|
|
crossSection: Double? = nil
|
|
) -> Double {
|
|
guard voltage != 0 else { return 0 }
|
|
let drop = voltageDrop(
|
|
length: length,
|
|
current: current,
|
|
voltage: voltage,
|
|
unitSystem: unitSystem,
|
|
crossSection: crossSection
|
|
)
|
|
return (drop / voltage) * 100
|
|
}
|
|
|
|
static func powerLoss(
|
|
length: Double,
|
|
current: Double,
|
|
voltage: Double,
|
|
unitSystem: UnitSystem,
|
|
crossSection: Double? = nil
|
|
) -> Double {
|
|
let drop = voltageDrop(
|
|
length: length,
|
|
current: current,
|
|
voltage: voltage,
|
|
unitSystem: unitSystem,
|
|
crossSection: crossSection
|
|
)
|
|
return current * drop
|
|
}
|
|
|
|
static func recommendedFuse(forCurrent current: Double) -> Double {
|
|
let target = (current * 1.25).rounded(.up)
|
|
return standardFuses.first(where: { $0 >= target }) ?? standardFuses.last ?? target
|
|
}
|
|
|
|
private static func guardAgainstZero(_ divisor: Double, calculation: () -> Double) -> Double {
|
|
guard divisor > 0 else { return 0 }
|
|
return calculation()
|
|
}
|
|
|
|
private static func crossSectionFromAWG(_ awg: Double) -> Double {
|
|
let index = standardAWG.firstIndex(of: Int(awg)) ?? -1
|
|
if index >= 0 && index < awgCrossSections.count {
|
|
return awgCrossSections[index]
|
|
}
|
|
return 0.75
|
|
}
|
|
|
|
/// Formats an AWG value for display: positive values as-is, negative as "X/0" notation.
|
|
static func formatAWG(_ awg: Double) -> String {
|
|
let intAWG = Int(awg)
|
|
switch intAWG {
|
|
case -1: return "1/0"
|
|
case -2: return "2/0"
|
|
case -3: return "3/0"
|
|
case -4: return "4/0"
|
|
default: return "\(intAWG)"
|
|
}
|
|
}
|
|
}
|