Files
Cable/Cable/Loads/ElectricalCalculations.swift
Stefan Lange-Hegermann ea3b60d75c Fix AWG notation, add alternator type, migrate to String(localized:)
- 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>
2026-03-27 10:37:53 +01:00

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