mechanical-tolerance-calculator 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +468 -235
- package/package.json +1 -1
- package/test.js +6 -4
package/index.js
CHANGED
|
@@ -61,95 +61,189 @@ function getCamcoStandardTolerancesFor(materialType) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Returns tolerance data for a given material type.
|
|
66
|
+
*
|
|
67
|
+
* - If `spec` is provided:
|
|
68
|
+
* → returns only that specific tolerance
|
|
69
|
+
* → returns an error object if the spec does not exist
|
|
70
|
+
*
|
|
71
|
+
* - If `spec` is not provided:
|
|
72
|
+
* → returns all available tolerances for the material type
|
|
73
|
+
*
|
|
74
|
+
* @param {string} executableMaterialType - Material type (e.g. shaft, bore)
|
|
75
|
+
* @param {string} [spec=""] - Optional tolerance specification (e.g. H7, h6)
|
|
76
|
+
*/
|
|
64
77
|
function returnTolerancesFor(executableMaterialType, spec = "") {
|
|
78
|
+
const materialTolerances = tolerances[executableMaterialType];
|
|
79
|
+
|
|
80
|
+
// Guard: invalid material type
|
|
81
|
+
if (!materialTolerances) {
|
|
82
|
+
return {
|
|
83
|
+
error: `Unknown material type: ${executableMaterialType}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If a specific spec is requested
|
|
65
88
|
if (spec) {
|
|
66
|
-
|
|
67
|
-
if (!Object.keys(allTolerances).includes(spec)) {
|
|
89
|
+
if (!materialTolerances[spec]) {
|
|
68
90
|
return {
|
|
69
|
-
error: `
|
|
70
|
-
|
|
71
|
-
)}`,
|
|
91
|
+
error: `Available specifications: ${Object.keys(
|
|
92
|
+
materialTolerances,
|
|
93
|
+
).join(", ")}`,
|
|
72
94
|
};
|
|
73
95
|
}
|
|
96
|
+
|
|
74
97
|
return {
|
|
75
98
|
type: executableMaterialType,
|
|
76
|
-
specification:
|
|
99
|
+
specification: materialTolerances[spec],
|
|
77
100
|
};
|
|
78
101
|
}
|
|
79
102
|
|
|
103
|
+
// Return all specs for the material
|
|
80
104
|
return {
|
|
81
105
|
type: executableMaterialType,
|
|
82
|
-
specifications:
|
|
106
|
+
specifications: materialTolerances,
|
|
83
107
|
};
|
|
84
108
|
}
|
|
85
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Validates a measurement input.
|
|
112
|
+
*
|
|
113
|
+
* Rules:
|
|
114
|
+
* - Must be a number (or numeric string)
|
|
115
|
+
* - Must not be NaN
|
|
116
|
+
* - Must be within a realistic measurement range
|
|
117
|
+
*
|
|
118
|
+
* @param {number|string} measurement
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
86
121
|
function isValidMeasurement(measurement) {
|
|
87
|
-
const
|
|
88
|
-
|
|
122
|
+
const value = Number(measurement);
|
|
123
|
+
|
|
124
|
+
return Number.isFinite(value) && value >= 0 && value < 1000;
|
|
89
125
|
}
|
|
90
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Derives the nominal size from a raw measurement
|
|
129
|
+
* based on material behavior (shaft vs bore).
|
|
130
|
+
*
|
|
131
|
+
* @param {number|string} measurement
|
|
132
|
+
* @param {"shafts"|"housingBores"|"shellBores"} materialType
|
|
133
|
+
* @param {number} THRESHOLD - allowable deviation before snapping to next nominal
|
|
134
|
+
* @returns {number|{error: string}}
|
|
135
|
+
*/
|
|
91
136
|
function parseNominalFromMeasurement(
|
|
92
137
|
measurement,
|
|
93
138
|
materialType,
|
|
94
139
|
THRESHOLD = 0.9,
|
|
95
140
|
) {
|
|
96
141
|
if (!isValidMeasurement(measurement)) {
|
|
97
|
-
return { error: "Measurement must be between 0
|
|
142
|
+
return { error: "Measurement must be between 0 and 1000." };
|
|
98
143
|
}
|
|
99
|
-
|
|
100
|
-
|
|
144
|
+
|
|
145
|
+
const value = Number(measurement);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* SHAFTS
|
|
149
|
+
* - Nominal is normally ABOVE the measurement
|
|
150
|
+
* - Upper deviation is 0
|
|
151
|
+
* - Rare cases allow slight overshoot (handled via threshold)
|
|
152
|
+
*/
|
|
101
153
|
if (materialType === "shafts") {
|
|
102
|
-
const
|
|
154
|
+
const ceilNominal = Math.ceil(value);
|
|
155
|
+
const deviationFromCeil = ceilNominal - value;
|
|
103
156
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
if (standardNominal - measurement >= THRESHOLD) {
|
|
107
|
-
return Math.floor(measurement);
|
|
108
|
-
}
|
|
109
|
-
return Math.ceil(measurement);
|
|
157
|
+
// If shaft is too far below the nominal, snap down
|
|
158
|
+
return deviationFromCeil >= THRESHOLD ? Math.floor(value) : ceilNominal;
|
|
110
159
|
}
|
|
111
160
|
|
|
112
|
-
|
|
113
|
-
|
|
161
|
+
/**
|
|
162
|
+
* BORES (housing / shell)
|
|
163
|
+
* - Nominal is normally BELOW the measurement
|
|
164
|
+
* - Lower deviation is 0
|
|
165
|
+
*/
|
|
114
166
|
if (materialType === "housingBores" || materialType === "shellBores") {
|
|
115
|
-
const
|
|
167
|
+
const floorNominal = Math.floor(value);
|
|
168
|
+
const deviationFromFloor = value - floorNominal;
|
|
116
169
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
: standardNominal;
|
|
170
|
+
// If bore grows too much above nominal, snap up
|
|
171
|
+
return deviationFromFloor >= THRESHOLD ? Math.ceil(value) : floorNominal;
|
|
120
172
|
}
|
|
121
173
|
|
|
122
|
-
|
|
123
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Fallback
|
|
176
|
+
* - Used only if material type is unknown
|
|
177
|
+
*/
|
|
178
|
+
return Math.round(value);
|
|
124
179
|
}
|
|
125
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Configuration defining how each material type
|
|
183
|
+
* maps to specifications, IT grades, and nominal matching rules.
|
|
184
|
+
*/
|
|
126
185
|
const MATERIAL_TYPE_CONFIG = {
|
|
127
186
|
shafts: {
|
|
128
187
|
specification: "h9",
|
|
129
188
|
itGrade: "IT5",
|
|
130
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Shafts:
|
|
192
|
+
* - Upper deviation = 0
|
|
193
|
+
* - Nominal sits at the top end of the range
|
|
194
|
+
*/
|
|
131
195
|
rangeMatch: (nominal, spec) =>
|
|
132
196
|
nominal > spec.minimum_diameter && nominal <= spec.maximum_diameter,
|
|
133
197
|
},
|
|
198
|
+
|
|
134
199
|
housingBores: {
|
|
135
200
|
specification: "H8",
|
|
136
201
|
itGrade: "IT6",
|
|
137
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Housing bores:
|
|
205
|
+
* - Lower deviation = 0
|
|
206
|
+
* - Nominal sits at the bottom end of the range
|
|
207
|
+
*/
|
|
138
208
|
rangeMatch: (nominal, spec) =>
|
|
139
209
|
nominal >= spec.minimum_diameter && nominal < spec.maximum_diameter,
|
|
140
210
|
},
|
|
211
|
+
|
|
141
212
|
shellBores: {
|
|
142
213
|
specification: "H9",
|
|
143
214
|
itGrade: "IT6",
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Shell bores:
|
|
218
|
+
* - Same rule as housing bores, different spec
|
|
219
|
+
*/
|
|
144
220
|
rangeMatch: (nominal, spec) =>
|
|
145
221
|
nominal >= spec.minimum_diameter && nominal < spec.maximum_diameter,
|
|
146
222
|
},
|
|
147
223
|
};
|
|
148
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Finds the specification that matches a given nominal
|
|
227
|
+
* using a material-specific range matching rule.
|
|
228
|
+
*
|
|
229
|
+
* @param {number} nominal
|
|
230
|
+
* @param {Array<Object>} specs
|
|
231
|
+
* @param {(nominal: number, spec: Object) => boolean} rangeMatchFn
|
|
232
|
+
* @returns {Object|null}
|
|
233
|
+
*/
|
|
149
234
|
function findMatchingSpec(nominal, specs, rangeMatchFn) {
|
|
150
|
-
|
|
235
|
+
if (!Array.isArray(specs)) return null;
|
|
236
|
+
|
|
237
|
+
return specs.find((spec) => rangeMatchFn(nominal, spec)) ?? null;
|
|
151
238
|
}
|
|
152
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Calculates numeric (computed) upper and lower bounds.
|
|
242
|
+
*
|
|
243
|
+
* Example:
|
|
244
|
+
* nominal = 200
|
|
245
|
+
* upper_deviation = 0.072 → 200.072
|
|
246
|
+
*/
|
|
153
247
|
function calculateComputedBounds(nominal, spec) {
|
|
154
248
|
return {
|
|
155
249
|
upperBound: parseComputedBound(nominal, spec.upper_deviation, 3),
|
|
@@ -157,6 +251,13 @@ function calculateComputedBounds(nominal, spec) {
|
|
|
157
251
|
};
|
|
158
252
|
}
|
|
159
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Calculates display-friendly (uncomputed) bounds.
|
|
256
|
+
*
|
|
257
|
+
* Example:
|
|
258
|
+
* 200 + 0.072
|
|
259
|
+
* 200 - 0.000
|
|
260
|
+
*/
|
|
160
261
|
function calculateUncomputedBounds(nominal, spec) {
|
|
161
262
|
return {
|
|
162
263
|
upperBound: parseUncomputedBound(nominal, spec.upper_deviation, "+"),
|
|
@@ -164,23 +265,55 @@ function calculateUncomputedBounds(nominal, spec) {
|
|
|
164
265
|
};
|
|
165
266
|
}
|
|
166
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Checks whether a measurement falls within
|
|
270
|
+
* the calculated specification bounds.
|
|
271
|
+
*
|
|
272
|
+
* @param {number|string} measurement
|
|
273
|
+
* @param {{ upperBound: number|string, lowerBound: number|string }} bounds
|
|
274
|
+
* @returns {boolean|{error: string}}
|
|
275
|
+
*/
|
|
167
276
|
function checkMeetsSpecification(measurement, bounds) {
|
|
168
277
|
if (!isValidMeasurement(measurement)) {
|
|
169
|
-
return { error: "Measurement must be between 0
|
|
278
|
+
return { error: "Measurement must be between 0 and 1000." };
|
|
170
279
|
}
|
|
171
|
-
const measure = parseStringFloat(measurement);
|
|
172
|
-
const upper = parseStringFloat(bounds.upperBound);
|
|
173
|
-
const lower = parseStringFloat(bounds.lowerBound);
|
|
174
280
|
|
|
175
|
-
|
|
281
|
+
const value = Number(measurement);
|
|
282
|
+
const upper = Number(bounds.upperBound);
|
|
283
|
+
const lower = Number(bounds.lowerBound);
|
|
284
|
+
|
|
285
|
+
if (![value, upper, lower].every(Number.isFinite)) {
|
|
286
|
+
return { error: "Invalid specification bounds." };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return value >= lower && value <= upper;
|
|
176
290
|
}
|
|
177
291
|
|
|
178
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Processes a single measurement for a given material type.
|
|
294
|
+
*
|
|
295
|
+
* Steps:
|
|
296
|
+
* 1. Validates the measurement.
|
|
297
|
+
* 2. Retrieves configuration for the material type.
|
|
298
|
+
* 3. Calculates the nominal diameter based on the measurement.
|
|
299
|
+
* 4. Finds the matching specification for the nominal.
|
|
300
|
+
* 5. Calculates numeric (computed) and display-friendly (uncomputed) bounds.
|
|
301
|
+
* 6. Checks if the measurement meets the specification.
|
|
302
|
+
* 7. Generates a human-readable outcome and reasoning.
|
|
303
|
+
*
|
|
304
|
+
* @param {"shafts"|"housingBores"|"shellBores"} materialType
|
|
305
|
+
* @param {number|string} measurement - The raw measurement value.
|
|
306
|
+
* @param {Object} tolerances - Tolerance data for the material type.
|
|
307
|
+
* @returns {Object} Processed measurement details, or error if invalid.
|
|
308
|
+
*/
|
|
309
|
+
function processOneMeasurement(materialType, measurement, tolerances) {
|
|
310
|
+
// 1. Validate the measurement
|
|
179
311
|
if (!isValidMeasurement(measurement)) {
|
|
180
|
-
return { error: "Measurement must be between 0
|
|
312
|
+
return { error: "Measurement must be between 0 and 1000." };
|
|
181
313
|
}
|
|
182
|
-
const config = MATERIAL_TYPE_CONFIG[materialType];
|
|
183
314
|
|
|
315
|
+
// 2. Get material configuration (specification, IT grade, range matching)
|
|
316
|
+
const config = MATERIAL_TYPE_CONFIG[materialType];
|
|
184
317
|
if (!config) {
|
|
185
318
|
return {
|
|
186
319
|
error: true,
|
|
@@ -188,16 +321,18 @@ function processMeasurement(materialType, measurement, tolerances) {
|
|
|
188
321
|
};
|
|
189
322
|
}
|
|
190
323
|
|
|
191
|
-
//
|
|
324
|
+
// 3. Derive nominal diameter from the measurement
|
|
192
325
|
const nominal = parseNominalFromMeasurement(measurement, materialType);
|
|
326
|
+
if (nominal?.error) {
|
|
327
|
+
return { error: true, message: nominal.error };
|
|
328
|
+
}
|
|
193
329
|
|
|
194
|
-
// Find
|
|
330
|
+
// 4. Find the specification that matches the nominal
|
|
195
331
|
const matchedSpec = findMatchingSpec(
|
|
196
332
|
nominal,
|
|
197
333
|
tolerances.specification,
|
|
198
334
|
config.rangeMatch,
|
|
199
335
|
);
|
|
200
|
-
|
|
201
336
|
if (!matchedSpec) {
|
|
202
337
|
return {
|
|
203
338
|
error: true,
|
|
@@ -206,13 +341,12 @@ function processMeasurement(materialType, measurement, tolerances) {
|
|
|
206
341
|
};
|
|
207
342
|
}
|
|
208
343
|
|
|
209
|
-
// Calculate bounds
|
|
210
|
-
const computedBounds = calculateComputedBounds(nominal, matchedSpec);
|
|
211
|
-
const uncomputedBounds = calculateUncomputedBounds(nominal, matchedSpec);
|
|
344
|
+
// 5. Calculate specification bounds
|
|
345
|
+
const computedBounds = calculateComputedBounds(nominal, matchedSpec); // numeric bounds for checking
|
|
346
|
+
const uncomputedBounds = calculateUncomputedBounds(nominal, matchedSpec); // human-readable bounds for display
|
|
212
347
|
|
|
213
|
-
// Check if measurement meets specification
|
|
348
|
+
// 6. Check if measurement meets the specification
|
|
214
349
|
const meetsSpec = checkMeetsSpecification(measurement, computedBounds);
|
|
215
|
-
|
|
216
350
|
const specMeetingReason = generateReasonForSpecs(
|
|
217
351
|
meetsSpec,
|
|
218
352
|
measurement,
|
|
@@ -220,16 +354,20 @@ function processMeasurement(materialType, measurement, tolerances) {
|
|
|
220
354
|
computedBounds.upperBound,
|
|
221
355
|
);
|
|
222
356
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
357
|
+
// 7. Determine human-readable outcome
|
|
358
|
+
const numericMeasurement = parseStringFloat(measurement);
|
|
359
|
+
let outcome;
|
|
360
|
+
if (numericMeasurement > computedBounds.upperBound) {
|
|
361
|
+
outcome = `${materialType} is over-sized.`;
|
|
362
|
+
} else if (numericMeasurement < computedBounds.lowerBound) {
|
|
363
|
+
outcome = `${materialType} is under-sized.`;
|
|
364
|
+
} else {
|
|
365
|
+
outcome = `${materialType} is in acceptable size.`;
|
|
366
|
+
}
|
|
230
367
|
|
|
368
|
+
// 8. Return structured result
|
|
231
369
|
return {
|
|
232
|
-
measurement:
|
|
370
|
+
measurement: numericMeasurement,
|
|
233
371
|
nominal,
|
|
234
372
|
specification: config.specification,
|
|
235
373
|
IT_grade: config.itGrade,
|
|
@@ -245,200 +383,272 @@ function processMeasurement(materialType, measurement, tolerances) {
|
|
|
245
383
|
};
|
|
246
384
|
}
|
|
247
385
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
...processedMeasurement,
|
|
259
|
-
meets_IT_tolerance: processedMeasurement.meets_specification.meetsSpec,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Checks a single measurement against Camco standard tolerances.
|
|
388
|
+
*
|
|
389
|
+
* Optional helper function for quick validation of one measurement.
|
|
390
|
+
*
|
|
391
|
+
* @param {"shafts"|"housingBores"|"shellBores"} materialType
|
|
392
|
+
* @param {number|string} measurement
|
|
393
|
+
* @returns {Object} Processed measurement details or error object
|
|
394
|
+
*/
|
|
263
395
|
function checkOneMeasurementFor(materialType, measurement) {
|
|
396
|
+
// 1. Validate measurement value
|
|
264
397
|
if (!isValidMeasurement(measurement)) {
|
|
265
|
-
return { error: "Measurement must be between 0
|
|
398
|
+
return { error: "Measurement must be between 0 and 1000." };
|
|
266
399
|
}
|
|
267
|
-
const camcoStandardTolerances = getCamcoStandardTolerancesFor(materialType);
|
|
268
400
|
|
|
401
|
+
// 2. Retrieve Camco standard tolerances for the material type
|
|
402
|
+
const camcoStandardTolerances = getCamcoStandardTolerancesFor(materialType);
|
|
269
403
|
if (camcoStandardTolerances.error) {
|
|
270
|
-
return camcoStandardTolerances;
|
|
404
|
+
return camcoStandardTolerances; // pass through the error
|
|
271
405
|
}
|
|
272
406
|
|
|
273
|
-
|
|
407
|
+
// 3. Ensure measurement is numeric
|
|
408
|
+
const numericMeasurement = Number(measurement);
|
|
409
|
+
if (!Number.isFinite(numericMeasurement)) {
|
|
274
410
|
return {
|
|
275
411
|
error: true,
|
|
276
412
|
message: "Invalid measurement value",
|
|
277
413
|
};
|
|
278
414
|
}
|
|
279
415
|
|
|
416
|
+
// 4. Process the measurement using standard tolerances
|
|
280
417
|
return processOneMeasurement(
|
|
281
418
|
camcoStandardTolerances.type,
|
|
282
|
-
|
|
419
|
+
numericMeasurement,
|
|
283
420
|
camcoStandardTolerances,
|
|
284
421
|
);
|
|
285
422
|
}
|
|
286
423
|
|
|
287
|
-
|
|
288
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Calculates a numeric upper or lower bound by adding the deviation to the nominal.
|
|
426
|
+
*
|
|
427
|
+
* @param {number} base - The nominal measurement.
|
|
428
|
+
* @param {number|string} value - The deviation (can be negative or string).
|
|
429
|
+
* @param {number} decimalCount - Number of decimals to round to.
|
|
430
|
+
* @returns {string} - Computed bound as a string with fixed decimals.
|
|
431
|
+
*/
|
|
432
|
+
function parseComputedBound(base, value, decimalCount = 3) {
|
|
433
|
+
const numericValue = parseStringFloat(value);
|
|
434
|
+
const bound = Number(base) + numericValue;
|
|
435
|
+
return bound.toFixed(decimalCount);
|
|
289
436
|
}
|
|
290
437
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Formats a human-readable bound string for display purposes.
|
|
440
|
+
* Example: "200.000 + 0.072" or "200.000 - 0.115"
|
|
441
|
+
*
|
|
442
|
+
* @param {number} nominal - Nominal value.
|
|
443
|
+
* @param {string|number} deviation - Deviation value (can start with "-" for negative).
|
|
444
|
+
* @param {"+"|"-"} sign - Sign to display for the deviation.
|
|
445
|
+
* @returns {string} - Formatted bound string.
|
|
446
|
+
*/
|
|
447
|
+
function parseUncomputedBound(nominal, deviation, sign) {
|
|
448
|
+
const numericNominal = parseToFixedThreeString(nominal);
|
|
449
|
+
|
|
450
|
+
// Handle negative deviation
|
|
451
|
+
if (typeof deviation === "string" && deviation.startsWith("-")) {
|
|
452
|
+
const positiveDeviation = deviation.slice(1);
|
|
453
|
+
return `${numericNominal} ${sign} ${parseToFixedThreeString(positiveDeviation)}`;
|
|
300
454
|
}
|
|
301
455
|
|
|
302
|
-
return (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
456
|
+
return `${numericNominal} ${sign} ${parseToFixedThreeString(deviation)}`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Utility to convert a number or string to a string with 3 decimals.
|
|
461
|
+
* @param {number|string} value
|
|
462
|
+
* @returns {string}
|
|
463
|
+
*/
|
|
464
|
+
function parseToFixedThreeString(value) {
|
|
465
|
+
const num = typeof value === "number" ? value : parseFloat(value);
|
|
466
|
+
return Number.isFinite(num) ? num.toFixed(3) : "0.000";
|
|
309
467
|
}
|
|
310
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Converts a number or numeric string to a string with 3 decimal places.
|
|
471
|
+
* If input is invalid, returns "0.000".
|
|
472
|
+
*
|
|
473
|
+
* @param {number|string} value
|
|
474
|
+
* @returns {string} - Number formatted as a string with 3 decimals.
|
|
475
|
+
*/
|
|
311
476
|
function parseToFixedThreeString(value) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
return value;
|
|
477
|
+
const num = typeof value === "number" ? value : parseFloat(value);
|
|
478
|
+
return Number.isFinite(num) ? num.toFixed(3) : "0.000";
|
|
316
479
|
}
|
|
317
480
|
|
|
318
481
|
/**
|
|
319
|
-
* Converts string
|
|
320
|
-
*
|
|
321
|
-
*
|
|
482
|
+
* Converts a string or number to a float.
|
|
483
|
+
* Safely handles null, undefined, or non-numeric strings by returning 0.
|
|
484
|
+
*
|
|
485
|
+
* @param {number|string} value - Value to convert to float
|
|
486
|
+
* @returns {number} - Parsed float number
|
|
322
487
|
*/
|
|
323
488
|
function parseStringFloat(value) {
|
|
324
|
-
|
|
325
|
-
if (value ===
|
|
326
|
-
return 0;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// If it's already a number, return it
|
|
330
|
-
if (typeof value === "number") {
|
|
331
|
-
return value;
|
|
332
|
-
}
|
|
489
|
+
if (value === null || value === undefined) return 0;
|
|
490
|
+
if (typeof value === "number") return value;
|
|
333
491
|
|
|
334
|
-
// Convert string to float
|
|
335
492
|
const parsed = parseFloat(value);
|
|
336
|
-
|
|
337
|
-
// Return 0 if parsing fails (NaN)
|
|
338
|
-
return isNaN(parsed) ? 0 : parsed;
|
|
493
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
339
494
|
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Processes a single measurement for a given material type.
|
|
498
|
+
* Validates the measurement and calculates its nominal, specification compliance,
|
|
499
|
+
* and IT tolerance based on the provided tolerances.
|
|
500
|
+
*
|
|
501
|
+
* @param {string} materialType - The type of material (e.g., "shafts", "housingBores", "shellBores")
|
|
502
|
+
* @param {number} measurement - The measurement value to process
|
|
503
|
+
* @param {object} tolerances - Tolerance definitions for the material type
|
|
504
|
+
* @returns {object} Processed measurement details including nominal, spec bounds, IT grade,
|
|
505
|
+
* and whether it meets specification
|
|
506
|
+
*/
|
|
340
507
|
function processIndividualMeasurement(materialType, measurement, tolerances) {
|
|
508
|
+
// Validate that the measurement is a valid number between 0 and 1000
|
|
341
509
|
if (!isValidMeasurement(measurement)) {
|
|
342
510
|
return { error: "Measurement must be between 0 to 1000." };
|
|
343
511
|
}
|
|
344
|
-
|
|
512
|
+
|
|
513
|
+
// Delegate actual processing to the generic processMeasurement function
|
|
514
|
+
const processedMeasurement = processOneMeasurement(
|
|
345
515
|
materialType,
|
|
346
516
|
measurement,
|
|
347
517
|
tolerances,
|
|
348
518
|
);
|
|
519
|
+
|
|
349
520
|
return processedMeasurement;
|
|
350
521
|
}
|
|
351
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Processes multiple measurements for a given material type.
|
|
525
|
+
* Determines spec compliance, IT tolerance, and final compliance.
|
|
526
|
+
*/
|
|
352
527
|
function checkMultipleMeasurementsFor(materialType, measurements) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
528
|
+
// 1. Validate measurements
|
|
529
|
+
const validationError = validateMeasurementsArray(measurements);
|
|
530
|
+
if (validationError) return validationError;
|
|
357
531
|
|
|
358
|
-
|
|
532
|
+
// 2. Get Camco standard tolerances
|
|
533
|
+
const camcoTolerances = getCamcoStandardTolerancesFor(materialType);
|
|
534
|
+
if (camcoTolerances.error) return camcoTolerances;
|
|
359
535
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
value: m,
|
|
365
|
-
error: "Invalid measurement: must be 0–1000",
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
});
|
|
536
|
+
// 3. Process all measurements individually
|
|
537
|
+
const results = measurements.map((m) =>
|
|
538
|
+
processIndividualMeasurement(camcoTolerances.type, m, camcoTolerances),
|
|
539
|
+
);
|
|
369
540
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
541
|
+
// 4. Determine most common nominal and farthest measurement
|
|
542
|
+
const mostOccuredNominal = findMostOccuredNominal(results);
|
|
543
|
+
const mostFarMeasurement = findFarthestMeasurement(
|
|
544
|
+
measurements,
|
|
545
|
+
mostOccuredNominal,
|
|
546
|
+
);
|
|
373
547
|
|
|
374
|
-
|
|
548
|
+
// 5. Base spec for the most common nominal
|
|
549
|
+
const baseSpec = results.find((r) => r.nominal === mostOccuredNominal);
|
|
550
|
+
const baseITValue = baseSpec.matched_spec[baseSpec.IT_grade];
|
|
375
551
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
552
|
+
// 6. Check IT tolerance and spec compliance
|
|
553
|
+
const { meetsIT, itReason } = checkITTolerance(
|
|
554
|
+
measurements,
|
|
555
|
+
baseITValue,
|
|
556
|
+
baseSpec.IT_grade,
|
|
557
|
+
);
|
|
558
|
+
const { meetsSpec, specReason } = checkSpecCompliance(
|
|
559
|
+
results,
|
|
560
|
+
baseSpec,
|
|
561
|
+
mostFarMeasurement,
|
|
380
562
|
);
|
|
381
563
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
(
|
|
564
|
+
// 7. Generate outcome messages
|
|
565
|
+
const generalizedOutcome = generateOutcomeMessage(
|
|
566
|
+
materialType,
|
|
567
|
+
mostFarMeasurement,
|
|
568
|
+
baseSpec,
|
|
569
|
+
meetsSpec,
|
|
570
|
+
meetsIT,
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
...baseSpec,
|
|
575
|
+
measurement: measurements,
|
|
576
|
+
meets_specification: { meetsSpec, reason: specReason },
|
|
577
|
+
meets_IT_Tolerance: { meetsIT, reason: itReason },
|
|
578
|
+
meets_final_compliance: meetsSpec && meetsIT,
|
|
579
|
+
generalized_outcome: generalizedOutcome,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** --- Helper Functions for checkMultipleMeasuremetsFor() start--- */
|
|
584
|
+
|
|
585
|
+
/** Validate the array of measurements */
|
|
586
|
+
function validateMeasurementsArray(measurements) {
|
|
587
|
+
const validationError = validateMeasurements(measurements);
|
|
588
|
+
if (validationError) return validationError;
|
|
589
|
+
|
|
590
|
+
const invalids = measurements
|
|
591
|
+
.map((m, idx) => (!isValidMeasurement(m) ? { index: idx, value: m } : null))
|
|
592
|
+
.filter(Boolean);
|
|
593
|
+
|
|
594
|
+
if (invalids.length > 0)
|
|
595
|
+
return { error: "Some measurements are invalid.", details: invalids };
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Find the most frequently occurring nominal */
|
|
600
|
+
function findMostOccuredNominal(results) {
|
|
601
|
+
const nominalCounts = {};
|
|
602
|
+
results.forEach(
|
|
603
|
+
(r) => (nominalCounts[r.nominal] = (nominalCounts[r.nominal] || 0) + 1),
|
|
411
604
|
);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
const baseSpec = results.find(
|
|
420
|
-
(result) => result.nominal === parseInt(mostOccuredNominal),
|
|
605
|
+
|
|
606
|
+
return parseInt(
|
|
607
|
+
Object.keys(nominalCounts).find(
|
|
608
|
+
(n) => nominalCounts[n] === Math.max(...Object.values(nominalCounts)),
|
|
609
|
+
),
|
|
421
610
|
);
|
|
422
|
-
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Find the measurement farthest from the most common nominal */
|
|
614
|
+
function findFarthestMeasurement(measurements, referenceNominal) {
|
|
615
|
+
return measurements.reduce(
|
|
616
|
+
(farthest, current) =>
|
|
617
|
+
Math.abs(current - referenceNominal) >
|
|
618
|
+
Math.abs(farthest - referenceNominal)
|
|
619
|
+
? current
|
|
620
|
+
: farthest,
|
|
621
|
+
measurements[0],
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Check IT tolerance */
|
|
626
|
+
function checkITTolerance(measurements, baseITValue, ITGrade) {
|
|
627
|
+
const largest = Math.max(...measurements);
|
|
628
|
+
const smallest = Math.min(...measurements);
|
|
629
|
+
const ITDifference = parseToFixedThreeString(largest - smallest);
|
|
423
630
|
|
|
424
631
|
const meetsIT = ITDifference <= baseITValue;
|
|
425
|
-
const
|
|
632
|
+
const reason = generateReasonForTolerances(
|
|
426
633
|
meetsIT,
|
|
427
|
-
|
|
428
|
-
|
|
634
|
+
largest,
|
|
635
|
+
smallest,
|
|
429
636
|
baseITValue,
|
|
430
|
-
|
|
637
|
+
ITGrade,
|
|
431
638
|
);
|
|
432
639
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return (
|
|
436
|
-
value >= baseSpec.computed_specification_bounds.lowerBound &&
|
|
437
|
-
value <= baseSpec.computed_specification_bounds.upperBound
|
|
438
|
-
);
|
|
439
|
-
});
|
|
640
|
+
return { meetsIT, itReason: reason };
|
|
641
|
+
}
|
|
440
642
|
|
|
441
|
-
|
|
643
|
+
/** Check if all measurements meet specification bounds */
|
|
644
|
+
function checkSpecCompliance(results, baseSpec, mostFarMeasurement) {
|
|
645
|
+
const meetsSpec = results.every(
|
|
646
|
+
(r) =>
|
|
647
|
+
r.measurement >= baseSpec.computed_specification_bounds.lowerBound &&
|
|
648
|
+
r.measurement <= baseSpec.computed_specification_bounds.upperBound,
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
const reason = generateReasonForSpecs(
|
|
442
652
|
meetsSpec,
|
|
443
653
|
mostFarMeasurement,
|
|
444
654
|
baseSpec.computed_specification_bounds.lowerBound,
|
|
@@ -446,95 +656,118 @@ function checkMultipleMeasurementsFor(materialType, measurements) {
|
|
|
446
656
|
baseSpec.specification,
|
|
447
657
|
);
|
|
448
658
|
|
|
659
|
+
return { meetsSpec, specReason: reason };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Generate a human-readable outcome message */
|
|
663
|
+
function generateOutcomeMessage(
|
|
664
|
+
materialType,
|
|
665
|
+
mostFarMeasurement,
|
|
666
|
+
baseSpec,
|
|
667
|
+
meetsSpec,
|
|
668
|
+
meetsIT,
|
|
669
|
+
) {
|
|
670
|
+
const isWithinSizeRange =
|
|
671
|
+
mostFarMeasurement >= baseSpec.computed_specification_bounds.lowerBound &&
|
|
672
|
+
mostFarMeasurement <= baseSpec.computed_specification_bounds.upperBound;
|
|
673
|
+
|
|
449
674
|
const isOverSized =
|
|
450
675
|
mostFarMeasurement > baseSpec.computed_specification_bounds.upperBound;
|
|
451
|
-
const isWithinSizeRange =
|
|
452
|
-
mostFarMeasurement <= baseSpec.computed_specification_bounds.upperBound &&
|
|
453
|
-
mostFarMeasurement >= baseSpec.computed_specification_bounds.lowerBound;
|
|
454
676
|
|
|
455
|
-
const
|
|
677
|
+
const sizeOutcome = isWithinSizeRange
|
|
456
678
|
? `${materialType} is acceptable in size.`
|
|
457
679
|
: isOverSized
|
|
458
680
|
? `${materialType} is over-sized.`
|
|
459
681
|
: `${materialType} is under-sized.`;
|
|
460
682
|
|
|
461
|
-
const
|
|
683
|
+
const ITOutcome =
|
|
462
684
|
isWithinSizeRange && meetsIT
|
|
463
685
|
? "And, it meets IT tolerance."
|
|
464
686
|
: !isWithinSizeRange && meetsIT
|
|
465
687
|
? "However, it meets IT tolerance."
|
|
466
688
|
: `${!isWithinSizeRange ? "And, " : "But, "}it fails IT tolerance.`;
|
|
467
689
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
// : !meetsIT && isWithinSizeRange
|
|
473
|
-
// ? `, but `
|
|
474
|
-
// : `, and `) +
|
|
475
|
-
// (meetsIT ? `meets IT tolerance.` : `doesn't meet IT tolerance.`);
|
|
690
|
+
const finalOutcome =
|
|
691
|
+
meetsSpec && meetsIT
|
|
692
|
+
? "Finally, it meets final compliance and is acceptable to use."
|
|
693
|
+
: "Finally, it doesn't meet final compliance and is not acceptable to use.";
|
|
476
694
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const outcome3 = final_compliance
|
|
480
|
-
? "Finally, it meets final compliance and is acceptable to use."
|
|
481
|
-
: "Finally, it doesn't meet final compliance and is not acceptable to use.";
|
|
482
|
-
return {
|
|
483
|
-
...baseSpec,
|
|
484
|
-
measurement: measurements,
|
|
485
|
-
meets_specification: { meetsSpec, reason: specMeetingReason },
|
|
486
|
-
meets_IT_Tolerance: { meetsIT, reason: itMeetingReason },
|
|
487
|
-
meets_final_compliance: final_compliance,
|
|
488
|
-
generalized_outcome: outcome1 + " " + outcome2 + " " + outcome3,
|
|
489
|
-
};
|
|
695
|
+
return `${sizeOutcome} ${ITOutcome} ${finalOutcome}`;
|
|
490
696
|
}
|
|
491
697
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
698
|
+
/**
|
|
699
|
+
* Generates a human-readable reason explaining whether a measurement
|
|
700
|
+
* meets the given specification bounds.
|
|
701
|
+
*
|
|
702
|
+
* @param {boolean} spec - Whether the measurement meets the specification
|
|
703
|
+
* @param {number} measurement - The actual measurement value
|
|
704
|
+
* @param {number|string} lowerBound - Lower bound of the specification
|
|
705
|
+
* @param {number|string} upperBound - Upper bound of the specification
|
|
706
|
+
* @param {string} specType - The type of specification (e.g., "H8", "h9")
|
|
707
|
+
* @returns {string} Reason describing compliance
|
|
708
|
+
*/
|
|
709
|
+
function generateReasonForSpecs(
|
|
710
|
+
spec,
|
|
711
|
+
measurement,
|
|
712
|
+
lowerBound,
|
|
713
|
+
upperBound,
|
|
714
|
+
specType,
|
|
715
|
+
) {
|
|
716
|
+
const formattedMeasurement = parseToFixedThreeString(measurement);
|
|
717
|
+
if (spec) {
|
|
718
|
+
return `${formattedMeasurement} falls between ${lowerBound} and ${upperBound}. So, the material meets ${specType} specification.`;
|
|
719
|
+
} else {
|
|
720
|
+
return `${formattedMeasurement} doesn't fall between ${lowerBound} and ${upperBound}. So, the material doesn't meet ${specType} specification.`;
|
|
497
721
|
}
|
|
498
|
-
return `${parseToFixedThreeString(
|
|
499
|
-
measurement,
|
|
500
|
-
)} doesn't fall between ${base1} and ${base2}. So, the material doesn't meet ${specType} specification.`;
|
|
501
722
|
}
|
|
502
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Generates a human-readable reason explaining whether the
|
|
726
|
+
* measurements meet the specified IT tolerance.
|
|
727
|
+
*
|
|
728
|
+
* @param {boolean} spec - Whether the tolerance condition is met
|
|
729
|
+
* @param {number} measurement1 - First measurement value
|
|
730
|
+
* @param {number} measurement2 - Second measurement value
|
|
731
|
+
* @param {number|string} toleranceValue - IT tolerance limit
|
|
732
|
+
* @param {string} toleranceType - Tolerance type (e.g., "IT5", "IT6")
|
|
733
|
+
* @returns {string} Reason describing tolerance compliance
|
|
734
|
+
*/
|
|
503
735
|
function generateReasonForTolerances(
|
|
504
736
|
spec,
|
|
505
737
|
measurement1,
|
|
506
738
|
measurement2,
|
|
507
|
-
|
|
739
|
+
toleranceValue,
|
|
508
740
|
toleranceType,
|
|
509
741
|
) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
742
|
+
const diff1 = parseToFixedThreeString(measurement1);
|
|
743
|
+
const diff2 = parseToFixedThreeString(measurement2);
|
|
744
|
+
if (spec) {
|
|
745
|
+
return `The difference between ${diff1} and ${diff2} is less than or equal to ${toleranceValue}. So, it meets ${toleranceType} Tolerance.`;
|
|
746
|
+
} else {
|
|
747
|
+
return `The difference between ${diff1} and ${diff2} is greater than ${toleranceValue}. So, it doesn't meet ${toleranceType} Tolerance.`;
|
|
516
748
|
}
|
|
517
|
-
return `The difference between ${parseToFixedThreeString(
|
|
518
|
-
measurement1,
|
|
519
|
-
)} and ${parseToFixedThreeString(measurement2)} is greater than ${base}. So, it doesn't meet ${toleranceType} Tolerance.`;
|
|
520
749
|
}
|
|
521
750
|
|
|
751
|
+
/**
|
|
752
|
+
* Validates that the input is a non-empty array of measurements.
|
|
753
|
+
*
|
|
754
|
+
* @param {Array<number>} measurements - Array of measurements to validate
|
|
755
|
+
* @returns {object|null} Returns error object if invalid, otherwise null
|
|
756
|
+
*/
|
|
522
757
|
function validateMeasurements(measurements) {
|
|
523
758
|
if (!Array.isArray(measurements)) {
|
|
524
|
-
return {
|
|
525
|
-
error: "Measurements must be an array of numbers",
|
|
526
|
-
};
|
|
759
|
+
return { error: "Measurements must be an array of numbers." };
|
|
527
760
|
}
|
|
528
761
|
|
|
529
762
|
if (measurements.length === 0) {
|
|
530
|
-
return {
|
|
531
|
-
error: "Measurements array cannot be empty",
|
|
532
|
-
};
|
|
763
|
+
return { error: "Measurements array cannot be empty." };
|
|
533
764
|
}
|
|
534
765
|
|
|
535
|
-
return null;
|
|
766
|
+
return null; // Valid
|
|
536
767
|
}
|
|
537
768
|
|
|
769
|
+
/** --- Helper Functions for checkMultipleMeasuremetsFor() end--- */
|
|
770
|
+
|
|
538
771
|
module.exports = {
|
|
539
772
|
getAllTolerancesFor,
|
|
540
773
|
getCamcoStandardTolerancesFor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mechanical-tolerance-calculator",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Calculates international standard specification and tolerances for bores, round bars and metals of mechanical units. For examples; H7, H8, H9, h8, h9 specifications and IT5/IT6 tolerances.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/test.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
checkMultipleMeasurementsFor,
|
|
3
|
+
checkOneMeasurementFor,
|
|
4
|
+
} = require("./index");
|
|
2
5
|
// console.log(checkMultipleMeasurementsFor("housing", [100.04, 100.05]));
|
|
3
6
|
// console.log(checkMultipleMeasurementsFor("housing", [100.04, 100.05, 95.06]));
|
|
4
|
-
console.log(
|
|
5
|
-
|
|
6
|
-
);
|
|
7
|
+
console.log(checkMultipleMeasurementsFor("housing", [99.99, 100.15, 100.2]));
|
|
8
|
+
console.log(checkOneMeasurementFor("shaft", 199.98));
|