Threads: a metric size without a coarse pitch suggests its fine pitches again

The thread forms (b4549d270) parse designations in thread_forms, whose refusal for a plain
M28 said "give it: M28x1" and, for M0 or M1e9, still offered a pitch. The hint the hole
command gave (the two coarsest ISO fine pitches that suit the diameter: M28x2 or M28x1.5) is
now thread_forms::no_coarse_pitch, used by both; a diameter no metric thread has is an
unknown metric thread. The test's "unknown thread" follows the forms' wording ("unknown
metric thread").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 22:10:51 -04:00
committed by Irakli Kourtis
parent f8c2fba077
commit 5cc0d5e9c4
3 changed files with 16 additions and 11 deletions
+13 -1
View File
@@ -400,7 +400,7 @@ impl ThreadForm {
}
None => {
let d = num(body).ok_or_else(bad)?;
let p = self.coarse(d).ok_or_else(|| format!("no coarse pitch for M{} (give it: M{}x1)", show(d), show(d)))?;
let p = self.coarse(d).ok_or_else(|| no_coarse_pitch(d).unwrap_or_else(bad))?;
(d, p, p * n)
}
}
@@ -663,6 +663,18 @@ fn inches(s: &str) -> Option<f64> {
num(s)
}
/// The refusal for a plain `M<d>` with no ISO coarse pitch: the two coarsest fine pitches to
/// give that suit the diameter. `None` for a diameter no metric thread has.
pub fn no_coarse_pitch(d: f64) -> Option<String> {
const PITCHES: [f64; 9] = [2.0, 1.5, 1.25, 1.0, 0.75, 0.5, 0.35, 0.25, 0.2];
if !(d.is_finite() && d > 0.2 && d <= 1e4) {
return None;
}
let first = PITCHES.iter().position(|p| *p <= 0.12 * d).unwrap_or(PITCHES.len() - 1);
let pitches: Vec<String> = PITCHES.iter().skip(first).take(2).filter(|p| **p < d).map(|p| format!("M{}x{}", show(d), show(*p))).collect();
(!pitches.is_empty()).then(|| format!("M{} has no coarse ISO pitch: give one, e.g. {}", show(d), pitches.join(" or ")))
}
fn show(v: f64) -> String {
let s = format!("{v:.6}");
s.trim_end_matches('0').trim_end_matches('.').to_string()
+1 -9
View File
@@ -2343,16 +2343,8 @@ fn clearance_diameter(size: &str, fit: &str) -> Option<f64> {
/// The pitches to try for a plain `M<d>` that has no ISO coarse pitch, `None` for anything else.
fn fine_only_hint(t: &str) -> Option<String> {
const PITCHES: [f64; 9] = [2.0, 1.5, 1.25, 1.0, 0.75, 0.5, 0.35, 0.25, 0.2];
let diameter = t.trim().strip_prefix(['M', 'm']).and_then(|d| d.trim().parse::<f64>().ok());
if let Some(d) = diameter.filter(|d| d.is_finite() && *d > 0.2 && *d <= 1e4) {
let first = PITCHES.iter().position(|p| *p <= 0.12 * d).unwrap_or(PITCHES.len() - 1);
let pitches: Vec<String> = PITCHES.iter().skip(first).take(2).filter(|p| **p < d).map(|p| format!("M{d}x{p}")).collect();
if !pitches.is_empty() {
return Some(format!("M{d} has no coarse ISO pitch: give one, e.g. {}", pitches.join(" or ")));
}
}
None
diameter.and_then(solidcraft_doc::thread_forms::no_coarse_pitch)
}
/// Largest nominal diameter automatic thread sizing knows (the last ISO coarse size).
+2 -1
View File
@@ -270,7 +270,8 @@ fn a_thread_without_a_coarse_pitch_suggests_explicit_pitches() {
}
for designation in ["MNaN", "Minf", "M-28", "M0", "M1e9", "M1e999", "M28x", "M28xNaN"] {
let e = s.execute("solid.thread", &json!({"face": [14, 0, 5], "designation": designation})).unwrap_err().to_string();
assert!(e.contains("unknown thread"), "{designation}: {e}");
// Each thread form names itself in its refusal: `unknown metric thread`.
assert!(e.contains("unknown metric thread"), "{designation}: {e}");
assert_eq!(s.doc.to_json(), saved);
}
run(&mut s, "solid.thread", json!({"face": [14, 0, 5], "designation": "M28x2"}));