1use geop_core_math::geop_error::{GeopError, GeopResult};
16use serde::{Deserialize, Serialize};
17
18#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct MetricSize {
21 pub name: &'static str,
23 pub diameter: f64,
25 pub pitch: f64,
26 pub tap_drill: f64,
27 pub clearance: [f64; 3],
29 pub counterbore: f64,
31 pub countersink: Option<f64>,
33}
34
35pub const TRIANGLE_HEIGHT: f64 = 0.866_025_403_784_438_6;
38
39impl MetricSize {
40 pub fn minor_diameter(&self) -> f64 {
42 self.diameter - 1.25 * TRIANGLE_HEIGHT * self.pitch
43 }
44
45 pub fn designation(&self) -> String {
47 format!("{}x{}", self.name, self.pitch)
48 }
49
50 pub fn clearance(&self, fit: Fit) -> f64 {
52 self.clearance[fit as usize]
53 }
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum Fit {
60 Close = 0,
62 #[default]
64 Normal = 1,
65 Loose = 2,
67}
68
69impl Fit {
70 pub const ALL: [Fit; 3] = [Fit::Close, Fit::Normal, Fit::Loose];
71
72 pub fn value(self) -> &'static str {
74 match self {
75 Fit::Close => "close",
76 Fit::Normal => "normal",
77 Fit::Loose => "loose",
78 }
79 }
80
81 pub fn label(self) -> &'static str {
82 match self {
83 Fit::Close => "Close",
84 Fit::Normal => "Normal",
85 Fit::Loose => "Loose",
86 }
87 }
88
89 pub fn from_value(value: &str) -> Option<Fit> {
90 Fit::ALL.into_iter().find(|f| f.value() == value)
91 }
92}
93
94const fn size(
95 name: &'static str,
96 diameter: f64,
97 pitch: f64,
98 tap_drill: f64,
99 clearance: [f64; 3],
100 counterbore: f64,
101 countersink: Option<f64>,
102) -> MetricSize {
103 MetricSize {
104 name,
105 diameter,
106 pitch,
107 tap_drill,
108 clearance,
109 counterbore,
110 countersink,
111 }
112}
113
114pub const SIZES: [MetricSize; 16] = [
116 size("M1.6", 1.6, 0.35, 1.25, [1.7, 1.8, 2.0], 3.5, Some(3.6)),
117 size("M2", 2.0, 0.4, 1.6, [2.2, 2.4, 2.6], 4.4, Some(4.4)),
118 size("M2.5", 2.5, 0.45, 2.05, [2.7, 2.9, 3.1], 5.5, Some(5.5)),
119 size("M3", 3.0, 0.5, 2.5, [3.2, 3.4, 3.6], 6.5, Some(6.3)),
120 size("M4", 4.0, 0.7, 3.3, [4.3, 4.5, 4.8], 8.0, Some(9.4)),
121 size("M5", 5.0, 0.8, 4.2, [5.3, 5.5, 5.8], 10.0, Some(10.4)),
122 size("M6", 6.0, 1.0, 5.0, [6.4, 6.6, 7.0], 11.0, Some(12.6)),
123 size("M8", 8.0, 1.25, 6.8, [8.4, 9.0, 10.0], 15.0, Some(17.3)),
124 size("M10", 10.0, 1.5, 8.5, [10.5, 11.0, 12.0], 18.0, Some(20.0)),
125 size(
126 "M12",
127 12.0,
128 1.75,
129 10.2,
130 [13.0, 13.5, 14.5],
131 20.0,
132 Some(24.0),
133 ),
134 size("M14", 14.0, 2.0, 12.0, [15.0, 15.5, 16.5], 24.0, Some(28.0)),
135 size("M16", 16.0, 2.0, 14.0, [17.0, 17.5, 18.5], 26.0, Some(32.0)),
136 size("M18", 18.0, 2.5, 15.5, [19.0, 20.0, 21.0], 30.0, Some(36.0)),
137 size("M20", 20.0, 2.5, 17.5, [21.0, 22.0, 24.0], 33.0, Some(40.0)),
138 size("M22", 22.0, 2.5, 19.5, [23.0, 24.0, 26.0], 36.0, None),
139 size("M24", 24.0, 3.0, 21.0, [25.0, 26.0, 28.0], 40.0, None),
140];
141
142pub fn metric(name: &str) -> GeopResult<&'static MetricSize> {
144 SIZES.iter().find(|s| s.name == name).ok_or_else(|| {
145 let known: Vec<&str> = SIZES.iter().map(|s| s.name).collect();
146 GeopError::new(format!(
147 "{name:?} is no ISO metric coarse thread size; there are {}",
148 known.join(", ")
149 ))
150 })
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
161 fn the_tables_are_consistent() {
162 for s in &SIZES {
163 assert!(
164 s.minor_diameter() < s.tap_drill && s.tap_drill < s.diameter,
165 "{s:?}"
166 );
167 assert!(s.diameter < s.clearance[0], "{s:?}");
168 assert!(
169 s.clearance[0] < s.clearance[1] && s.clearance[1] < s.clearance[2],
170 "{s:?}"
171 );
172 assert!(s.clearance[2] < s.counterbore, "{s:?}");
173 if let Some(cs) = s.countersink {
174 assert!(s.clearance[2] < cs, "{s:?}");
175 }
176 }
177 assert_eq!(metric("M6").unwrap().designation(), "M6x1");
178 assert_eq!(metric("M8").unwrap().designation(), "M8x1.25");
179 assert!(metric("M7").is_err());
180 }
181}