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.
Files changed (3) hide show
  1. package/index.js +468 -235
  2. package/package.json +1 -1
  3. 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
- const allTolerances = tolerances[executableMaterialType];
67
- if (!Object.keys(allTolerances).includes(spec)) {
89
+ if (!materialTolerances[spec]) {
68
90
  return {
69
- error: `Currently available specifications are ${Object.keys(
70
- allTolerances,
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: tolerances[executableMaterialType][spec],
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: tolerances[executableMaterialType],
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 num = parseFloat(measurement);
88
- return !isNaN(num) && num >= 0 && num < 1000;
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 to 1000." };
142
+ return { error: "Measurement must be between 0 and 1000." };
98
143
  }
99
- // For shafts: upper_deviation is 0, so measurement ≤ nominal
100
- // Therefore, nominal must be ceiling of measurement
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 standardNominal = Math.ceil(measurement); //a standard shaft will always have measurements less than the nominal
154
+ const ceilNominal = Math.ceil(value);
155
+ const deviationFromCeil = ceilNominal - value;
103
156
 
104
- //however, in some cases, we get shafts going beyond the upper deviation
105
- //so, we work with a threshold of 0.10 (meaning, a shaft can only go upto 0.10 of it's upper deviation)
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
- // For bores: lower_deviation is 0, so measurement ≥ nominal
113
- // Therefore, nominal must be floor of measurement
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 standardNominal = Math.floor(measurement);
167
+ const floorNominal = Math.floor(value);
168
+ const deviationFromFloor = value - floorNominal;
116
169
 
117
- return measurement - standardNominal >= THRESHOLD
118
- ? Math.ceil(measurement)
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
- // Default: round to nearest
123
- return Math.round(measurement);
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
- return specs.find((spec) => rangeMatchFn(nominal, spec)) || null;
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 to 1000." };
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
- return measure >= lower && measure <= upper;
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
- function processMeasurement(materialType, measurement, tolerances) {
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 to 1000." };
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
- // Calculate nominal diameter
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 matching specification
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
- const outcome =
224
- measurement > computedBounds.upperBound
225
- ? `${materialType} is over-sized.`
226
- : measurement >= computedBounds.lowerBound &&
227
- measurement <= computedBounds.upperBound
228
- ? `${materialType} is in acceptable size.`
229
- : `${materialType} is under-sized.`;
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: parseStringFloat(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
- function processOneMeasurement(materialType, measurement, tolerances) {
249
- if (!isValidMeasurement(measurement)) {
250
- return { error: "Measurement must be between 0 to 1000." };
251
- }
252
- const processedMeasurement = processMeasurement(
253
- materialType,
254
- measurement,
255
- tolerances,
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 to 1000." };
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
- if (typeof measurement !== "number" || isNaN(measurement)) {
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
- measurement,
419
+ numericMeasurement,
283
420
  camcoStandardTolerances,
284
421
  );
285
422
  }
286
423
 
287
- function parseComputedBound(base, value, decimalCount) {
288
- return Number(base + parseStringFloat(value)).toFixed(decimalCount);
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
- function parseUncomputedBound(value1, value2, sign) {
292
- if (value2.startsWith("-")) {
293
- return (
294
- parseToFixedThreeString(value1) +
295
- " " +
296
- sign +
297
- " " +
298
- parseToFixedThreeString(value2.slice(1, value2.length))
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
- parseToFixedThreeString(value1) +
304
- " " +
305
- sign +
306
- " " +
307
- parseToFixedThreeString(value2)
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
- if (typeof value === "number") {
313
- return value.toFixed(3);
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 float values to actual float numbers
320
- * @param {string} value - The string representation of a float number
321
- * @returns {number} - The parsed float number
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
- // Handle edge cases
325
- if (value === null || value === undefined) {
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
- const processedMeasurement = processMeasurement(
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
- const validated = validateMeasurements(measurements);
354
- if (validated) {
355
- return validated;
356
- }
528
+ // 1. Validate measurements
529
+ const validationError = validateMeasurementsArray(measurements);
530
+ if (validationError) return validationError;
357
531
 
358
- const errors = [];
532
+ // 2. Get Camco standard tolerances
533
+ const camcoTolerances = getCamcoStandardTolerancesFor(materialType);
534
+ if (camcoTolerances.error) return camcoTolerances;
359
535
 
360
- measurements.forEach((m, index) => {
361
- if (!isValidMeasurement(m)) {
362
- errors.push({
363
- index,
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
- if (errors.length > 0) {
371
- return { error: "Some measurements are invalid.", details: errors };
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
- const camcoStandardTolerances = getCamcoStandardTolerancesFor(materialType);
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
- let largestMeasurement = Math.max(...measurements);
377
- let smallestMeasurement = Math.min(...measurements);
378
- let ITDifference = parseToFixedThreeString(
379
- largestMeasurement - smallestMeasurement,
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
- const nominals = {};
383
- let count = 0;
384
- // let withInSpecs = [];
385
- const results = measurements.map((measurement) => {
386
- const result = processIndividualMeasurement(
387
- camcoStandardTolerances.type,
388
- measurement,
389
- camcoStandardTolerances,
390
- );
391
- // withInSpecs.push(result.meets_specification.meetsSpec);
392
- const nominal = result.nominal;
393
-
394
- // count occurrences
395
- nominals[nominal] = (nominals[nominal] || 0) + 1;
396
-
397
- // if (
398
- // Math.abs(result.nominal - result.measurement) >
399
- // Math.abs(result.nominal - mostFarMeasurement)
400
- // ) {
401
- // mostFarMeasurement = result.measurement;
402
- // }
403
-
404
- return result;
405
- });
406
-
407
- let countOfMostOccuredNominal = Math.max(...Object.values(nominals));
408
-
409
- let mostOccuredNominal = Object.keys(nominals).find(
410
- (nominal) => nominals[nominal] === countOfMostOccuredNominal,
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
- let mostFarMeasurement = measurements.reduce((farthest, current) => {
413
- return Math.abs(current - mostOccuredNominal) >
414
- Math.abs(farthest - mostOccuredNominal)
415
- ? current
416
- : farthest;
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
- const baseITValue = baseSpec.matched_spec[baseSpec.IT_grade];
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 itMeetingReason = generateReasonForTolerances(
632
+ const reason = generateReasonForTolerances(
426
633
  meetsIT,
427
- largestMeasurement,
428
- smallestMeasurement,
634
+ largest,
635
+ smallest,
429
636
  baseITValue,
430
- baseSpec.IT_grade,
637
+ ITGrade,
431
638
  );
432
639
 
433
- const meetsSpec = results.every((r) => {
434
- const value = r.measurement;
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
- const specMeetingReason = generateReasonForSpecs(
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 outcome1 = isWithinSizeRange
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 outcome2 =
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
- // const outcome2 = meetsIT ? ""
469
-
470
- // (isWithinSizeRange && meetsIT
471
- // ? `, and `
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
- const final_compliance = meetsIT === true && meetsSpec === true;
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
- function generateReasonForSpecs(spec, measurement, base1, base2, specType) {
493
- if (spec === true) {
494
- return `${parseToFixedThreeString(
495
- measurement,
496
- )} falls between ${base1} and ${base2}. So, the material meets ${specType} specification.`;
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
- base,
739
+ toleranceValue,
508
740
  toleranceType,
509
741
  ) {
510
- if (spec === true) {
511
- return `The difference between ${parseToFixedThreeString(
512
- measurement1,
513
- )} and ${parseToFixedThreeString(
514
- measurement2,
515
- )} is less than or equal to ${base}. So, it meets ${toleranceType} Tolerance.`;
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.1",
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 { checkMultipleMeasurementsFor } = require("./index");
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
- checkMultipleMeasurementsFor("housing", [100, 32, 32, 1, 100, 100]),
6
- );
7
+ console.log(checkMultipleMeasurementsFor("housing", [99.99, 100.15, 100.2]));
8
+ console.log(checkOneMeasurementFor("shaft", 199.98));