1use serde::{Deserialize, Serialize};
6
7use crate::CalcError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum DeviceType {
12 B1,
13 B2,
14 B3,
15 B4,
16 B5,
17 A6,
18 A7,
19 A8,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct SpacingInput {
25 pub voltage: f64,
27 pub device_type: DeviceType,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct SpacingResult {
34 pub spacing_mils: f64,
36 pub spacing_mm: f64,
38}
39
40#[rustfmt::skip]
44const TABLE: [[f64; 9]; 8] = [
45 [ 1.97, 1.97, 3.94, 3.94, 7.87, 7.87, 7.87, 7.87, 9.84 ], [ 3.94, 3.94, 25.17, 25.17, 25.17, 49.21, 49.21, 49.21, 98.43 ], [ 3.94, 3.94, 25.17, 59.06, 125.98, 125.98, 251.97, 492.13, 492.13 ], [ 2.95, 2.95, 11.81, 11.81, 31.50, 31.50, 31.50, 31.50, 62.99 ], [ 2.95, 2.95, 5.12, 5.12, 15.75, 15.75, 15.75, 15.75, 31.50 ], [ 5.12, 5.12, 5.12, 5.12, 15.75, 15.75, 15.75, 15.75, 31.50 ], [ 5.12, 9.84, 15.75, 19.69, 31.50, 31.50, 31.50, 31.50, 59.06 ], [ 5.12, 9.84, 31.50, 39.37, 62.99, 62.99, 62.99, 62.99, 118.11 ], ];
55
56#[rustfmt::skip]
59const EXTRAP: [(f64, f64); 8] = [
60 (0.098425, 9.8425), (0.196850, 98.4252), (0.984252, 492.1260), (0.120070, 62.9900), (0.120070, 31.4961), (0.120070, 31.4961), (0.120070, 59.0551), (0.240157, 118.1100), ];
69
70fn device_index(d: DeviceType) -> usize {
71 match d {
72 DeviceType::B1 => 0,
73 DeviceType::B2 => 1,
74 DeviceType::B3 => 2,
75 DeviceType::B4 => 3,
76 DeviceType::B5 => 4,
77 DeviceType::A6 => 5,
78 DeviceType::A7 => 6,
79 DeviceType::A8 => 7,
80 }
81}
82
83fn voltage_column(voltage: f64) -> usize {
84 if voltage <= 15.0 {
85 0
86 } else if voltage <= 30.0 {
87 1
88 } else if voltage <= 50.0 {
89 2
90 } else if voltage <= 100.0 {
91 3
92 } else if voltage <= 150.0 {
93 4
94 } else if voltage <= 170.0 {
95 5
96 } else if voltage <= 250.0 {
97 6
98 } else if voltage <= 300.0 {
99 7
100 } else {
101 8
102 }
103}
104
105pub fn spacing(input: &SpacingInput) -> Result<SpacingResult, CalcError> {
111 if input.voltage < 0.0 {
112 return Err(CalcError::OutOfRange {
113 name: "voltage",
114 value: input.voltage,
115 expected: ">= 0",
116 });
117 }
118
119 let row = device_index(input.device_type);
120
121 let spacing_mils = if input.voltage > 500.0 {
122 let (slope, intercept) = EXTRAP[row];
123 (input.voltage - 500.0) * slope + intercept
124 } else {
125 let col = voltage_column(input.voltage);
126 TABLE[row][col]
127 };
128
129 Ok(SpacingResult {
130 spacing_mils,
131 spacing_mm: spacing_mils * 0.0254,
132 })
133}
134
135#[cfg(test)]
136mod tests {
137 use approx::assert_relative_eq;
138
139 use super::*;
140
141 fn lookup(voltage: f64, device_type: DeviceType) -> SpacingResult {
142 spacing(&SpacingInput { voltage, device_type }).unwrap()
143 }
144
145 #[test]
146 fn b1_10v() {
147 let r = lookup(10.0, DeviceType::B1);
148 assert_relative_eq!(r.spacing_mils, 1.97, epsilon = 1e-6);
149 }
150
151 #[test]
152 fn b3_40v() {
153 let r = lookup(40.0, DeviceType::B3);
154 assert_relative_eq!(r.spacing_mils, 25.17, epsilon = 1e-6);
155 }
156
157 #[test]
158 fn b1_600v_extrapolation() {
159 let r = lookup(600.0, DeviceType::B1);
160 let expected = (600.0 - 500.0) * 0.098425 + 9.8425;
161 assert_relative_eq!(r.spacing_mils, expected, epsilon = 1e-6);
162 assert_relative_eq!(r.spacing_mils, 19.685, epsilon = 1e-3);
163 }
164
165 #[test]
166 fn boundary_0v() {
167 let r = lookup(0.0, DeviceType::B1);
169 assert_relative_eq!(r.spacing_mils, TABLE[0][0], epsilon = 1e-9);
170 }
171
172 #[test]
173 fn boundary_15v() {
174 let r = lookup(15.0, DeviceType::B1);
176 assert_relative_eq!(r.spacing_mils, TABLE[0][0], epsilon = 1e-9);
177 }
178
179 #[test]
180 fn boundary_16v() {
181 let r = lookup(16.0, DeviceType::B1);
183 assert_relative_eq!(r.spacing_mils, TABLE[0][1], epsilon = 1e-9);
184 }
185
186 #[test]
187 fn boundary_30v() {
188 let r = lookup(30.0, DeviceType::B1);
190 assert_relative_eq!(r.spacing_mils, TABLE[0][1], epsilon = 1e-9);
191 }
192
193 #[test]
194 fn boundary_500v() {
195 let r = lookup(500.0, DeviceType::B1);
197 assert_relative_eq!(r.spacing_mils, TABLE[0][8], epsilon = 1e-9);
198 }
199
200 #[test]
201 fn boundary_501v_extrapolation() {
202 let r = lookup(501.0, DeviceType::B1);
204 let expected = 1.0 * 0.098425 + 9.8425;
205 assert_relative_eq!(r.spacing_mils, expected, epsilon = 1e-6);
206 }
207
208 #[test]
209 fn mm_conversion() {
210 let r = lookup(10.0, DeviceType::B2);
211 assert_relative_eq!(r.spacing_mm, r.spacing_mils * 0.0254, epsilon = 1e-9);
212 }
213
214 #[test]
215 fn rejects_negative_voltage() {
216 let result = spacing(&SpacingInput {
217 voltage: -1.0,
218 device_type: DeviceType::B1,
219 });
220 assert!(matches!(result, Err(CalcError::OutOfRange { name: "voltage", .. })));
221 }
222}