@toolpath/tool-scraper 2.0.0 → 2.2.0
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/dist/columns.d.ts +5 -3
- package/dist/columns.js +11 -5
- package/dist/conventions.d.ts +54 -0
- package/dist/conventions.js +59 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +30 -0
- package/dist/families/emuge.d.ts +3 -1
- package/dist/families/emuge.js +8 -4
- package/dist/family.d.ts +16 -2
- package/dist/holding.d.ts +396 -0
- package/dist/holding.js +360 -0
- package/dist/index.d.ts +23 -13
- package/dist/index.js +23 -13
- package/dist/node/cad-mirror.d.ts +58 -1
- package/dist/node/cad-mirror.js +56 -8
- package/dist/node/cli.d.ts +4 -1
- package/dist/node/cli.js +198 -19
- package/dist/node/holder-import.d.ts +223 -0
- package/dist/node/holder-import.js +379 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +1 -0
- package/dist/node/paths.d.ts +16 -0
- package/dist/node/paths.js +20 -0
- package/dist/profiles.d.ts +265 -0
- package/dist/profiles.js +295 -0
- package/dist/records.d.ts +57 -12
- package/dist/records.js +37 -13
- package/dist/registry.d.ts +86 -5
- package/dist/registry.js +149 -8
- package/dist/vendors/destinytool/records.js +8 -0
- package/dist/vendors/emuge/records.d.ts +83 -3
- package/dist/vendors/emuge/records.js +184 -16
- package/dist/vendors/kennametal/family.d.ts +119 -0
- package/dist/vendors/kennametal/family.js +155 -0
- package/dist/vendors/kennametal/holding.d.ts +35 -0
- package/dist/vendors/kennametal/holding.js +112 -0
- package/dist/vendors/kennametal/index.d.ts +2 -0
- package/dist/vendors/kennametal/index.js +2 -0
- package/dist/vendors/kennametal/records.d.ts +16 -0
- package/dist/vendors/kennametal/records.js +32 -0
- package/dist/vendors/kennametal/scrape.d.ts +25 -2
- package/dist/vendors/kennametal/scrape.js +28 -3
- package/dist/vendors/maritool/holding.d.ts +79 -0
- package/dist/vendors/maritool/holding.js +164 -0
- package/dist/vendors/maritool/index.d.ts +1 -0
- package/dist/vendors/maritool/index.js +1 -0
- package/dist/vendors/maritool/scrape.d.ts +37 -13
- package/dist/vendors/maritool/scrape.js +53 -14
- package/dist/vendors/regofix/holding.d.ts +35 -0
- package/dist/vendors/regofix/holding.js +108 -0
- package/dist/vendors/regofix/index.d.ts +1 -0
- package/dist/vendors/regofix/index.js +1 -0
- package/dist/vendors/regofix/scrape.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kennametal's and WIDIA's toolholding column vocabulary, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* The counterpart of `records.ts` for a holder and a collet: every decision
|
|
5
|
+
* about *what a record is* lives in `holding.ts`, and what lives here is which
|
|
6
|
+
* of this vendor's columns answers each question. That is the same line
|
|
7
|
+
* `vendors/kennametal/records.ts` draws for cutting tools.
|
|
8
|
+
*
|
|
9
|
+
* ## What this platform states as a fact rather than a column
|
|
10
|
+
*
|
|
11
|
+
* Almost everything. `taper`, `contact`, `clamping`, `style` and `unit` are all
|
|
12
|
+
* per-family constants here, declared in `families/kennametal.ts` with a
|
|
13
|
+
* citation each, because Kennametal sells one interface and one clamping mode
|
|
14
|
+
* per family — its dual-contact BT30 is a separate line (BTKV\*) with its own
|
|
15
|
+
* family code. The two vendors whose holders vary row by row are the ones whose
|
|
16
|
+
* mappers read a column instead.
|
|
17
|
+
*
|
|
18
|
+
* ## Two published columns this deliberately does not carry
|
|
19
|
+
*
|
|
20
|
+
* - **`L1FC`**, the BTKV30 line's "Gage Length Face Contact", 0.998 mm shorter
|
|
21
|
+
* than `L1` on every row. It is not a second measurement of one thing: it is
|
|
22
|
+
* the gage length *in a face-contact spindle*, so which of the two is true is
|
|
23
|
+
* a fact about the machine rather than about the holder. Carrying both would
|
|
24
|
+
* put two numbers named "gage" on one record with nothing to say which one
|
|
25
|
+
* stickout arithmetic should use. `contact` records that the holder has the
|
|
26
|
+
* geometry; `gaugeLength` stays `L1`. Promote it the day something reads a
|
|
27
|
+
* spindle's contact mode, and change `gaugeLength` with it rather than
|
|
28
|
+
* showing both.
|
|
29
|
+
* - **Torque figures, actuation-screw drive size, weight and `D21`.** Published,
|
|
30
|
+
* and dropped under `records.ToolRecord`'s standing rule: add a field when
|
|
31
|
+
* something displays it, not before.
|
|
32
|
+
*/
|
|
33
|
+
import { CAD_COLUMN, COLLET_DESIGNATION_COLUMN, COLLET_SERIES_COLUMN } from '../../conventions.js';
|
|
34
|
+
import { familyBrand } from '../../family.js';
|
|
35
|
+
import { checkUnitAgreement, clampingMode, colletRecord, contactMode, dim, holderRecord, holdingFact, published, } from '../../holding.js';
|
|
36
|
+
import { consoleWarn } from '../../scrape.js';
|
|
37
|
+
import { CATALOG_NUMBER, MATERIAL_NUMBER } from './records.js';
|
|
38
|
+
/**
|
|
39
|
+
* The dimensional labels whose two unit columns are worth cross-checking.
|
|
40
|
+
*
|
|
41
|
+
* Every dimension either record carries, and no more: a label nothing reads
|
|
42
|
+
* cannot produce a wrong number, so warning about it is noise. `holding.dim`
|
|
43
|
+
* reads the native column and never the other one, which is what makes this a
|
|
44
|
+
* report rather than a gate.
|
|
45
|
+
*/
|
|
46
|
+
const HOLDER_LABELS = ['D1', 'L1', 'L2', 'L9', 'V', 'D2', 'D11'];
|
|
47
|
+
const COLLET_LABELS = ['CCCN', 'CCCX', 'D1', 'BDX', 'LF', 'L'];
|
|
48
|
+
/** How a part names itself in a warning or a refusal. */
|
|
49
|
+
function subject(row) {
|
|
50
|
+
return `${row[CATALOG_NUMBER] ?? ''} (${row[MATERIAL_NUMBER] ?? ''})`;
|
|
51
|
+
}
|
|
52
|
+
/** One Kennametal or WIDIA holder row -> one {@link HolderRecord}. */
|
|
53
|
+
function holder(row, family, options = {}) {
|
|
54
|
+
const warn = options.warn ?? consoleWarn;
|
|
55
|
+
const what = subject(row);
|
|
56
|
+
const unit = holdingFact(family, 'unit', family.unit);
|
|
57
|
+
for (const label of HOLDER_LABELS)
|
|
58
|
+
checkUnitAgreement(row, label, what, warn);
|
|
59
|
+
// Kennametal publishes no description column for toolholding, and `''` is the
|
|
60
|
+
// honest answer where a vendor publishes none — `records.ToolRecord.description`
|
|
61
|
+
// states the rule and the reason: a description that restates the catalog
|
|
62
|
+
// number puts one string in two fields.
|
|
63
|
+
return holderRecord({
|
|
64
|
+
brand: familyBrand(family),
|
|
65
|
+
materialNumber: published(row[MATERIAL_NUMBER], what, 'material number'),
|
|
66
|
+
catalogNumber: published(row[CATALOG_NUMBER], what, 'catalog number'),
|
|
67
|
+
description: '',
|
|
68
|
+
unit,
|
|
69
|
+
taper: holdingFact(family, 'taper', family.taper),
|
|
70
|
+
contact: contactMode(holdingFact(family, 'contact', family.contact), what),
|
|
71
|
+
clamping: clampingMode(holdingFact(family, 'clamping', family.clamping), what),
|
|
72
|
+
style: holdingFact(family, 'style', family.style),
|
|
73
|
+
colletSeries: row[COLLET_SERIES_COLUMN] || null,
|
|
74
|
+
bore: dim(row, 'D1', unit),
|
|
75
|
+
gaugeLength: published(dim(row, 'L1', unit), what, 'L1 gage length'),
|
|
76
|
+
usableLength: dim(row, 'L2', unit),
|
|
77
|
+
clampingLength: dim(row, 'L9', unit),
|
|
78
|
+
adjustmentRange: dim(row, 'V', unit),
|
|
79
|
+
bodyDiameter: dim(row, 'D2', unit),
|
|
80
|
+
lockNutDiameter: dim(row, 'D11', unit),
|
|
81
|
+
cadModelUrl: row[CAD_COLUMN] || null,
|
|
82
|
+
// This platform publishes a STEP model and no 2D profile. A DXF column would
|
|
83
|
+
// be a claim about the data, and false — the call `conventions.CAD_DXF_COLUMN`
|
|
84
|
+
// records.
|
|
85
|
+
cadDxfUrl: null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** One Kennametal or WIDIA collet row -> one {@link ColletRecord}. */
|
|
89
|
+
function collet(row, family, options = {}) {
|
|
90
|
+
const warn = options.warn ?? consoleWarn;
|
|
91
|
+
const what = subject(row);
|
|
92
|
+
const unit = holdingFact(family, 'unit', family.unit);
|
|
93
|
+
for (const label of COLLET_LABELS)
|
|
94
|
+
checkUnitAgreement(row, label, what, warn);
|
|
95
|
+
return colletRecord({
|
|
96
|
+
brand: familyBrand(family),
|
|
97
|
+
materialNumber: published(row[MATERIAL_NUMBER], what, 'material number'),
|
|
98
|
+
catalogNumber: published(row[CATALOG_NUMBER], what, 'catalog number'),
|
|
99
|
+
description: '',
|
|
100
|
+
unit,
|
|
101
|
+
series: published(row[COLLET_DESIGNATION_COLUMN], what, 'collet series'),
|
|
102
|
+
style: holdingFact(family, 'style', family.style),
|
|
103
|
+
nominal: dim(row, 'D1', unit),
|
|
104
|
+
clampMin: published(dim(row, 'CCCN', unit), what, 'CCCN clamping minimum'),
|
|
105
|
+
clampMax: published(dim(row, 'CCCX', unit), what, 'CCCX clamping maximum'),
|
|
106
|
+
bodyDiameter: dim(row, 'BDX', unit),
|
|
107
|
+
functionalLength: dim(row, 'LF', unit),
|
|
108
|
+
overallLength: dim(row, 'L', unit),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** The toolholding half of the adapter contract `registry` looks up by brand. */
|
|
112
|
+
export const HOLDING_MAPPERS = { holder, collet };
|
|
@@ -52,6 +52,22 @@ export declare const CATALOG_NUMBER = "ISO Catalog Number";
|
|
|
52
52
|
* absent coating column is a table that states no coating, not a scrape fault.
|
|
53
53
|
*/
|
|
54
54
|
export declare const COATING = "Coating";
|
|
55
|
+
/**
|
|
56
|
+
* The column {@link productLine} reads, and the one this adapter names rather
|
|
57
|
+
* than the vendor.
|
|
58
|
+
*
|
|
59
|
+
* `conventions.FAMILY_TITLE_COLUMN` is vendor-neutral because a second vendor
|
|
60
|
+
* publishing a family title should write the same column; this one is not,
|
|
61
|
+
* because it holds a *segment* of that title that this adapter chose to split
|
|
62
|
+
* out. A vendor whose product line arrives some other way has no business
|
|
63
|
+
* writing here — EMUGE reads its own `product line` and `Geometry` columns and
|
|
64
|
+
* never sees this one.
|
|
65
|
+
*
|
|
66
|
+
* Both columns are written by `scrape.scrapeFamily` under `familyTitle`, and
|
|
67
|
+
* a CSV scraped before that option existed has neither — which is why
|
|
68
|
+
* {@link productLine} reads it as absent rather than required.
|
|
69
|
+
*/
|
|
70
|
+
export declare const PRODUCT_LINE_COLUMN = "Product Line";
|
|
55
71
|
/**
|
|
56
72
|
* A drill, in the family's native unit system per its `unit` fact.
|
|
57
73
|
*
|
|
@@ -55,6 +55,35 @@ export const CATALOG_NUMBER = 'ISO Catalog Number';
|
|
|
55
55
|
* absent coating column is a table that states no coating, not a scrape fault.
|
|
56
56
|
*/
|
|
57
57
|
export const COATING = 'Coating';
|
|
58
|
+
/**
|
|
59
|
+
* The column {@link productLine} reads, and the one this adapter names rather
|
|
60
|
+
* than the vendor.
|
|
61
|
+
*
|
|
62
|
+
* `conventions.FAMILY_TITLE_COLUMN` is vendor-neutral because a second vendor
|
|
63
|
+
* publishing a family title should write the same column; this one is not,
|
|
64
|
+
* because it holds a *segment* of that title that this adapter chose to split
|
|
65
|
+
* out. A vendor whose product line arrives some other way has no business
|
|
66
|
+
* writing here — EMUGE reads its own `product line` and `Geometry` columns and
|
|
67
|
+
* never sees this one.
|
|
68
|
+
*
|
|
69
|
+
* Both columns are written by `scrape.scrapeFamily` under `familyTitle`, and
|
|
70
|
+
* a CSV scraped before that option existed has neither — which is why
|
|
71
|
+
* {@link productLine} reads it as absent rather than required.
|
|
72
|
+
*/
|
|
73
|
+
export const PRODUCT_LINE_COLUMN = 'Product Line';
|
|
74
|
+
/**
|
|
75
|
+
* The vendor's own product line for this row, or null where the CSV has none.
|
|
76
|
+
*
|
|
77
|
+
* **An absent column is not a fault.** It is a table scraped without
|
|
78
|
+
* `scrape.FamilyOptions.familyTitle`, or a family page the vendor publishes
|
|
79
|
+
* with no heading, and both are rows that are otherwise complete. `null` is
|
|
80
|
+
* the answer `records.ToolRecord.productLine` defines for exactly this — see
|
|
81
|
+
* its docstring on why that is not `''`.
|
|
82
|
+
*/
|
|
83
|
+
function productLine(row) {
|
|
84
|
+
const stated = (row[PRODUCT_LINE_COLUMN] ?? '').trim();
|
|
85
|
+
return stated === '' ? null : stated;
|
|
86
|
+
}
|
|
58
87
|
/**
|
|
59
88
|
* One canonical dimension, or null when this family maps or publishes none.
|
|
60
89
|
*
|
|
@@ -160,6 +189,7 @@ export function drillRecord(row, family, columns) {
|
|
|
160
189
|
vendor: BRANDS[familyBrand(family)].vendor,
|
|
161
190
|
materialNumber: what,
|
|
162
191
|
catalogNumber: row[CATALOG_NUMBER] ?? '',
|
|
192
|
+
productLine: productLine(row),
|
|
163
193
|
// Kennametal publishes no description column on any table. The catalog
|
|
164
194
|
// number is already on the record, and repeating it here said nothing —
|
|
165
195
|
// see `records.ToolRecord.description`.
|
|
@@ -210,6 +240,7 @@ export function tapRecord(row, family, columns) {
|
|
|
210
240
|
// carry the size. The catalog number no longer leads it — it is already on
|
|
211
241
|
// the record, and a description that restates one is not a description.
|
|
212
242
|
description: tdz,
|
|
243
|
+
productLine: productLine(row),
|
|
213
244
|
kind: 'tap',
|
|
214
245
|
unit,
|
|
215
246
|
substrate: fact(family, 'bmc', family.bmc),
|
|
@@ -251,6 +282,7 @@ export function endmillRecord(row, family, columns) {
|
|
|
251
282
|
catalogNumber: row[CATALOG_NUMBER] ?? '',
|
|
252
283
|
// No description column here either. See the drill mapper.
|
|
253
284
|
description: '',
|
|
285
|
+
productLine: productLine(row),
|
|
254
286
|
kind: 'endmill',
|
|
255
287
|
unit,
|
|
256
288
|
substrate: fact(family, 'bmc', family.bmc),
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { Fetcher } from '../../fetch.js';
|
|
27
27
|
import { type AemBrandName } from '../../identity.js';
|
|
28
|
-
import type
|
|
28
|
+
import { type ScrapeResult } from '../../scrape.js';
|
|
29
29
|
export declare const BASE: string;
|
|
30
30
|
/**
|
|
31
31
|
* The Hybris/Solr facet string that scopes a request to a family's active
|
|
@@ -101,11 +101,34 @@ export declare function parseVariantTable(html: string): VariantTable;
|
|
|
101
101
|
* data row.
|
|
102
102
|
*/
|
|
103
103
|
export declare function columnNames(header: readonly Cell[]): (string | null)[];
|
|
104
|
+
/** What {@link scrapeFamily} accepts beyond its tags. */
|
|
105
|
+
export interface FamilyOptions {
|
|
106
|
+
/**
|
|
107
|
+
* Also read the family page's own title, and tag every row with it.
|
|
108
|
+
*
|
|
109
|
+
* **Opt-in, and a second request.** The variants table states no product
|
|
110
|
+
* line — the vendor puts it in the `h1` above the table, which is a
|
|
111
|
+
* different AEM resource — so this is the one thing here that costs a
|
|
112
|
+
* request the table did not. A caller that only wants dimensions should not
|
|
113
|
+
* pay for it, and every existing caller keeps the transport it had.
|
|
114
|
+
*
|
|
115
|
+
* See `family.ts` for what the title is and which part of it becomes
|
|
116
|
+
* `records.ToolRecord.productLine`.
|
|
117
|
+
*/
|
|
118
|
+
readonly familyTitle?: boolean;
|
|
119
|
+
/** Milliseconds between the two requests. Zero in tests. */
|
|
120
|
+
readonly delayMs?: number;
|
|
121
|
+
}
|
|
104
122
|
/**
|
|
105
123
|
* Scrape one family into rows.
|
|
106
124
|
*
|
|
107
125
|
* `tags` is a sequence of `[name, value]` pairs appended to every row as
|
|
108
126
|
* constant columns — used to tag facts the table doesn't state, e.g. the
|
|
109
127
|
* thread system on a tap family.
|
|
128
|
+
*
|
|
129
|
+
* `options.familyTitle` adds two more of exactly that kind. They are tags
|
|
130
|
+
* rather than parsed columns because that is what they are: one string per
|
|
131
|
+
* family, constant down its whole table, which is the case the `tags` seam was
|
|
132
|
+
* built for.
|
|
110
133
|
*/
|
|
111
|
-
export declare function scrapeFamily(fetcher: Fetcher, code: string, brand?: AemBrandName, tags?: readonly Tag[]): Promise<ScrapeResult>;
|
|
134
|
+
export declare function scrapeFamily(fetcher: Fetcher, code: string, brand?: AemBrandName, tags?: readonly Tag[], options?: FamilyOptions): Promise<ScrapeResult>;
|
|
@@ -24,8 +24,12 @@
|
|
|
24
24
|
* characters.
|
|
25
25
|
*/
|
|
26
26
|
import { Parser } from 'htmlparser2';
|
|
27
|
+
import { FAMILY_TITLE_COLUMN } from '../../conventions.js';
|
|
27
28
|
import { VendorResponseError } from '../../errors.js';
|
|
28
29
|
import { BRANDS } from '../../identity.js';
|
|
30
|
+
import { pause, REQUEST_DELAY_MS } from '../../scrape.js';
|
|
31
|
+
import { fetchFamily } from './family.js';
|
|
32
|
+
import { PRODUCT_LINE_COLUMN } from './records.js';
|
|
29
33
|
export const BASE = 'https://www.{host}/us/en/products/fam/_jcr_content/root/' +
|
|
30
34
|
'responsivegrid/{node}.variants.{code}.html' +
|
|
31
35
|
'?query={query}&uom=metric';
|
|
@@ -194,16 +198,37 @@ export function columnNames(header) {
|
|
|
194
198
|
* `tags` is a sequence of `[name, value]` pairs appended to every row as
|
|
195
199
|
* constant columns — used to tag facts the table doesn't state, e.g. the
|
|
196
200
|
* thread system on a tap family.
|
|
201
|
+
*
|
|
202
|
+
* `options.familyTitle` adds two more of exactly that kind. They are tags
|
|
203
|
+
* rather than parsed columns because that is what they are: one string per
|
|
204
|
+
* family, constant down its whole table, which is the case the `tags` seam was
|
|
205
|
+
* built for.
|
|
197
206
|
*/
|
|
198
|
-
export async function scrapeFamily(fetcher, code, brand = 'kennametal', tags = []) {
|
|
207
|
+
export async function scrapeFamily(fetcher, code, brand = 'kennametal', tags = [], options = {}) {
|
|
199
208
|
const url = variantsUrl(code, brand);
|
|
200
209
|
const { header, rows: dataRows } = parseVariantTable(await fetcher.text(url));
|
|
201
210
|
if (header === null) {
|
|
202
211
|
throw new VendorResponseError(`family ${code}`, 'the vendor returned no variants');
|
|
203
212
|
}
|
|
213
|
+
// After the table, so a family the vendor no longer publishes fails on the
|
|
214
|
+
// table it has no rows for rather than on a title nobody would have read.
|
|
215
|
+
const titled = [];
|
|
216
|
+
if (options.familyTitle === true) {
|
|
217
|
+
await pause(options.delayMs ?? REQUEST_DELAY_MS);
|
|
218
|
+
const { title, productLine } = await fetchFamily(fetcher, code, brand);
|
|
219
|
+
// A page with no heading writes no columns, rather than a column of empty
|
|
220
|
+
// strings that reads as a vendor stating an empty name. `unionHeader` is
|
|
221
|
+
// not in play here — this header is positional — so an absent tag is
|
|
222
|
+
// simply a narrower CSV.
|
|
223
|
+
if (title !== '')
|
|
224
|
+
titled.push([FAMILY_TITLE_COLUMN, title]);
|
|
225
|
+
if (productLine !== null)
|
|
226
|
+
titled.push([PRODUCT_LINE_COLUMN, productLine]);
|
|
227
|
+
}
|
|
204
228
|
const names = columnNames(header);
|
|
205
229
|
const kept = names.filter((name) => name !== null);
|
|
206
|
-
const
|
|
230
|
+
const allTags = [...tags, ...titled];
|
|
231
|
+
const csvHeader = [...kept, ...allTags.map(([name]) => name)];
|
|
207
232
|
const rows = dataRows.map((row) => {
|
|
208
233
|
// `dataRows` is filtered to rows exactly as long as the header, and
|
|
209
234
|
// `names` has one entry per header cell, so a length mismatch here means
|
|
@@ -218,7 +243,7 @@ export async function scrapeFamily(fetcher, code, brand = 'kennametal', tags = [
|
|
|
218
243
|
if (name !== null)
|
|
219
244
|
out[name] = row[index]?.[0] ?? '';
|
|
220
245
|
});
|
|
221
|
-
for (const [name, value] of
|
|
246
|
+
for (const [name, value] of allTags)
|
|
222
247
|
out[name] = value;
|
|
223
248
|
return out;
|
|
224
249
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MariTool's toolholding column vocabulary, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* The vendor at the far end of the range this package covers: where Kennametal
|
|
5
|
+
* states a holder's taper, contact, clamping mode, style and unit as five
|
|
6
|
+
* per-family constants, MariTool states every one of them per part.
|
|
7
|
+
* `families/maritool.ts` declares **no facts at all** for exactly that reason —
|
|
8
|
+
* each CSV is one spindle taper holding three clamping styles, the HSK file
|
|
9
|
+
* holds nine sizes, and `Gage Length` is metric on some parts and imperial on
|
|
10
|
+
* others inside one category page. So this mapper reads columns where the
|
|
11
|
+
* Kennametal one reads facts, and the record shape is the same either way.
|
|
12
|
+
*
|
|
13
|
+
* ## The record's unit comes from the gage length
|
|
14
|
+
*
|
|
15
|
+
* `scrape.holderRow` promotes `Gage Length` into the `conventions.GAGE_COLUMNS`
|
|
16
|
+
* pair with **exactly one cell filled**, and which one is filled is the only
|
|
17
|
+
* statement about unit system this vendor makes per part. That is what
|
|
18
|
+
* {@link HOLDING_MAPPERS} reads, and a row with both cells filled is refused
|
|
19
|
+
* rather than resolved: it would mean the scraper had changed shape, and
|
|
20
|
+
* picking one would hide that.
|
|
21
|
+
*
|
|
22
|
+
* ## `Shank Size` is the bore, and a bare cell is inches
|
|
23
|
+
*
|
|
24
|
+
* MariTool publishes no `D1`. Its shrink-fit and hydraulic holders state the
|
|
25
|
+
* shank they take under the vendor's own `Shank Size` label, which
|
|
26
|
+
* `scrape.holderRow` carries verbatim and unsuffixed — the adapter deliberately
|
|
27
|
+
* does not promote it, because a promoted column carries a unit suffix and
|
|
28
|
+
* these families declare no unit for one to be taken from.
|
|
29
|
+
*
|
|
30
|
+
* The cell takes three shapes across the 293 non-collet holders in the catalog,
|
|
31
|
+
* and {@link parseShankSize} reads all three: `.500`, `10mm`, and
|
|
32
|
+
* `.1181 (3mm)` — a decimal inch with the vendor's own metric annotation beside
|
|
33
|
+
* it. **A cell that names no unit is inches**, which is a claim about this
|
|
34
|
+
* vendor and worth the evidence: every bare value published is a fractional
|
|
35
|
+
* inch size (`.125`, `.1875`, `.250`, `.3125`, `.375`, `.4375`, `.500`, `.625`,
|
|
36
|
+
* `.750`, `1.0`, `1.25`, `1.5`), and the annotated form proves the reading —
|
|
37
|
+
* `.1181` is 3 mm in inches, to four places. One part in the catalog states a
|
|
38
|
+
* bare inch shank on a holder gaged in millimetres (`HSK40E-SF.125-45`), which
|
|
39
|
+
* is converted rather than warned about: the record's unit is itself promoted
|
|
40
|
+
* off another cell here, so the two disagreeing is this vendor's shape rather
|
|
41
|
+
* than a fault.
|
|
42
|
+
*
|
|
43
|
+
* ## Two published columns this does not carry
|
|
44
|
+
*
|
|
45
|
+
* - **`Nose Diameter`**, on 200-odd parts. It measures the holder's nose and
|
|
46
|
+
* `bodyDiameter` is `D2`, the body — promoting one into a field named for the
|
|
47
|
+
* other is precisely the collision `conventions` warns about at length. It
|
|
48
|
+
* also mixes `.870` with `.870 inches` in one column, which is a second
|
|
49
|
+
* reason to leave it as the receipt of what the vendor said.
|
|
50
|
+
* - **`Collet Grip Range`.** `scrape.holderRow` says why: it is the ER series'
|
|
51
|
+
* range restated on the holder's page, a pure function of `Collet Size`, and
|
|
52
|
+
* a real capacity comes from a collet family joined on `CST`.
|
|
53
|
+
*/
|
|
54
|
+
import { type UnitSystem } from '../../conventions.js';
|
|
55
|
+
import { type HoldingMappers } from '../../holding.js';
|
|
56
|
+
/** MariTool's own label for the cell {@link parseShankSize} reads a bore from. */
|
|
57
|
+
export declare const SHANK_SIZE_LABEL = "Shank Size";
|
|
58
|
+
/**
|
|
59
|
+
* MariTool's own label for the lock-nut diameter — `D11`, on five parts.
|
|
60
|
+
*
|
|
61
|
+
* Carried where `Nose Diameter` is not, because this one names exactly what the
|
|
62
|
+
* record's field names: the outside diameter of the collet nut, which is what
|
|
63
|
+
* decides whether a holder clears a fixture.
|
|
64
|
+
*/
|
|
65
|
+
export declare const COLLET_NUT_DIAMETER_LABEL = "Collet Nut Outside Diameter";
|
|
66
|
+
/** One cell as a diameter and the system it is stated in. Null where unreadable. */
|
|
67
|
+
export declare function parseShankSize(cell: string): {
|
|
68
|
+
value: number;
|
|
69
|
+
stated: UnitSystem;
|
|
70
|
+
} | null;
|
|
71
|
+
/**
|
|
72
|
+
* The toolholding half of the adapter contract `registry` looks up by brand.
|
|
73
|
+
*
|
|
74
|
+
* Holders only: MariTool sells collets and this package does not scrape them,
|
|
75
|
+
* so there is no `collet` mapper and `registry.toHolding` refuses a request for
|
|
76
|
+
* one naming what this brand does map. A partial table is the honest state for
|
|
77
|
+
* a kind nobody has read the columns of.
|
|
78
|
+
*/
|
|
79
|
+
export declare const HOLDING_MAPPERS: HoldingMappers;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MariTool's toolholding column vocabulary, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* The vendor at the far end of the range this package covers: where Kennametal
|
|
5
|
+
* states a holder's taper, contact, clamping mode, style and unit as five
|
|
6
|
+
* per-family constants, MariTool states every one of them per part.
|
|
7
|
+
* `families/maritool.ts` declares **no facts at all** for exactly that reason —
|
|
8
|
+
* each CSV is one spindle taper holding three clamping styles, the HSK file
|
|
9
|
+
* holds nine sizes, and `Gage Length` is metric on some parts and imperial on
|
|
10
|
+
* others inside one category page. So this mapper reads columns where the
|
|
11
|
+
* Kennametal one reads facts, and the record shape is the same either way.
|
|
12
|
+
*
|
|
13
|
+
* ## The record's unit comes from the gage length
|
|
14
|
+
*
|
|
15
|
+
* `scrape.holderRow` promotes `Gage Length` into the `conventions.GAGE_COLUMNS`
|
|
16
|
+
* pair with **exactly one cell filled**, and which one is filled is the only
|
|
17
|
+
* statement about unit system this vendor makes per part. That is what
|
|
18
|
+
* {@link HOLDING_MAPPERS} reads, and a row with both cells filled is refused
|
|
19
|
+
* rather than resolved: it would mean the scraper had changed shape, and
|
|
20
|
+
* picking one would hide that.
|
|
21
|
+
*
|
|
22
|
+
* ## `Shank Size` is the bore, and a bare cell is inches
|
|
23
|
+
*
|
|
24
|
+
* MariTool publishes no `D1`. Its shrink-fit and hydraulic holders state the
|
|
25
|
+
* shank they take under the vendor's own `Shank Size` label, which
|
|
26
|
+
* `scrape.holderRow` carries verbatim and unsuffixed — the adapter deliberately
|
|
27
|
+
* does not promote it, because a promoted column carries a unit suffix and
|
|
28
|
+
* these families declare no unit for one to be taken from.
|
|
29
|
+
*
|
|
30
|
+
* The cell takes three shapes across the 293 non-collet holders in the catalog,
|
|
31
|
+
* and {@link parseShankSize} reads all three: `.500`, `10mm`, and
|
|
32
|
+
* `.1181 (3mm)` — a decimal inch with the vendor's own metric annotation beside
|
|
33
|
+
* it. **A cell that names no unit is inches**, which is a claim about this
|
|
34
|
+
* vendor and worth the evidence: every bare value published is a fractional
|
|
35
|
+
* inch size (`.125`, `.1875`, `.250`, `.3125`, `.375`, `.4375`, `.500`, `.625`,
|
|
36
|
+
* `.750`, `1.0`, `1.25`, `1.5`), and the annotated form proves the reading —
|
|
37
|
+
* `.1181` is 3 mm in inches, to four places. One part in the catalog states a
|
|
38
|
+
* bare inch shank on a holder gaged in millimetres (`HSK40E-SF.125-45`), which
|
|
39
|
+
* is converted rather than warned about: the record's unit is itself promoted
|
|
40
|
+
* off another cell here, so the two disagreeing is this vendor's shape rather
|
|
41
|
+
* than a fault.
|
|
42
|
+
*
|
|
43
|
+
* ## Two published columns this does not carry
|
|
44
|
+
*
|
|
45
|
+
* - **`Nose Diameter`**, on 200-odd parts. It measures the holder's nose and
|
|
46
|
+
* `bodyDiameter` is `D2`, the body — promoting one into a field named for the
|
|
47
|
+
* other is precisely the collision `conventions` warns about at length. It
|
|
48
|
+
* also mixes `.870` with `.870 inches` in one column, which is a second
|
|
49
|
+
* reason to leave it as the receipt of what the vendor said.
|
|
50
|
+
* - **`Collet Grip Range`.** `scrape.holderRow` says why: it is the ER series'
|
|
51
|
+
* range restated on the holder's page, a pure function of `Collet Size`, and
|
|
52
|
+
* a real capacity comes from a collet family joined on `CST`.
|
|
53
|
+
*/
|
|
54
|
+
import { CAD_COLUMN, CAD_DXF_COLUMN, COLLET_SERIES_COLUMN, CONTACT_COLUMN, DESCRIPTION_COLUMN, GAGE_COLUMNS, } from '../../conventions.js';
|
|
55
|
+
import { IncompletePartError, VendorResponseError } from '../../errors.js';
|
|
56
|
+
import { familyBrand } from '../../family.js';
|
|
57
|
+
import { asUnit, clampingMode, contactMode, holderRecord, published, } from '../../holding.js';
|
|
58
|
+
import { fractionValue } from '../../measure.js';
|
|
59
|
+
import { CLAMPING_COLUMN, MATERIAL_COLUMN, STYLE_COLUMN, TAPER_COLUMN } from './scrape.js';
|
|
60
|
+
/** MariTool's own label for the cell {@link parseShankSize} reads a bore from. */
|
|
61
|
+
export const SHANK_SIZE_LABEL = 'Shank Size';
|
|
62
|
+
/**
|
|
63
|
+
* MariTool's own label for the lock-nut diameter — `D11`, on five parts.
|
|
64
|
+
*
|
|
65
|
+
* Carried where `Nose Diameter` is not, because this one names exactly what the
|
|
66
|
+
* record's field names: the outside diameter of the collet nut, which is what
|
|
67
|
+
* decides whether a holder clears a fixture.
|
|
68
|
+
*/
|
|
69
|
+
export const COLLET_NUT_DIAMETER_LABEL = 'Collet Nut Outside Diameter';
|
|
70
|
+
/**
|
|
71
|
+
* A `Shank Size` cell: a decimal, an optional unit, an optional annotation.
|
|
72
|
+
*
|
|
73
|
+
* The annotation is captured only so it can be dropped — it is the vendor's own
|
|
74
|
+
* conversion of the same dimension (`.1181 (3mm)`), not a second measurement,
|
|
75
|
+
* and reading it would put two sizes on one holder.
|
|
76
|
+
*
|
|
77
|
+
* `inches` leads the unit alternation because a regex alternation is ordered and
|
|
78
|
+
* `in` would otherwise match the first two letters of `inches` and leave `ches`
|
|
79
|
+
* unmatched — the same trap `parseGageLength` documents.
|
|
80
|
+
*/
|
|
81
|
+
const SHANK_SIZE = /^(?<value>\d*\.?\d+)\s*(?<unit>mm|inches|inch|in)?\s*(?:\([^)]*\))?$/i;
|
|
82
|
+
/** One cell as a diameter and the system it is stated in. Null where unreadable. */
|
|
83
|
+
export function parseShankSize(cell) {
|
|
84
|
+
const parsed = SHANK_SIZE.exec(cell.trim())?.groups;
|
|
85
|
+
if (parsed === undefined)
|
|
86
|
+
return null;
|
|
87
|
+
const value = fractionValue(parsed['value'] ?? '');
|
|
88
|
+
if (value === null || value <= 0)
|
|
89
|
+
return null;
|
|
90
|
+
const unit = (parsed['unit'] ?? '').toLowerCase();
|
|
91
|
+
return { value, stated: unit === 'mm' ? 'millimeters' : 'inches' };
|
|
92
|
+
}
|
|
93
|
+
/** How a part names itself in a warning or a refusal. */
|
|
94
|
+
function subject(row) {
|
|
95
|
+
return `${row[MATERIAL_COLUMN] ?? ''} (${row[DESCRIPTION_COLUMN] ?? ''})`;
|
|
96
|
+
}
|
|
97
|
+
/** The gage length and, with it, the unit system this record is in. */
|
|
98
|
+
function gage(row, what) {
|
|
99
|
+
const inches = fractionValue(row[GAGE_COLUMNS.inches] ?? '');
|
|
100
|
+
const millimeters = fractionValue(row[GAGE_COLUMNS.millimeters] ?? '');
|
|
101
|
+
if (inches !== null && millimeters !== null) {
|
|
102
|
+
throw new VendorResponseError(what, `publishes a gage length in both unit columns (${inches} in and ` +
|
|
103
|
+
`${millimeters} mm) — the scrape fills exactly one, and which one is ` +
|
|
104
|
+
`filled is the only unit system this vendor states per part`);
|
|
105
|
+
}
|
|
106
|
+
if (inches !== null)
|
|
107
|
+
return { unit: 'inches', gaugeLength: inches };
|
|
108
|
+
if (millimeters !== null)
|
|
109
|
+
return { unit: 'millimeters', gaugeLength: millimeters };
|
|
110
|
+
throw new IncompletePartError(what, 'publishes no gage length in either unit column');
|
|
111
|
+
}
|
|
112
|
+
/** One of MariTool's own unsuffixed diameter cells, in `unit`. */
|
|
113
|
+
function diameter(row, label, unit, what) {
|
|
114
|
+
const cell = (row[label] ?? '').trim();
|
|
115
|
+
if (cell === '')
|
|
116
|
+
return null;
|
|
117
|
+
const measured = parseShankSize(cell);
|
|
118
|
+
if (measured === null) {
|
|
119
|
+
throw new VendorResponseError(what, `${label} is ${JSON.stringify(cell)}, which is not a diameter this ` +
|
|
120
|
+
`package can read — add its shape to SHANK_SIZE once it is clear what ` +
|
|
121
|
+
`the vendor means by it`);
|
|
122
|
+
}
|
|
123
|
+
return asUnit(measured.value, measured.stated, unit);
|
|
124
|
+
}
|
|
125
|
+
/** One MariTool holder row -> one {@link HolderRecord}. */
|
|
126
|
+
function holder(row, family) {
|
|
127
|
+
const what = subject(row);
|
|
128
|
+
const { unit, gaugeLength } = gage(row, what);
|
|
129
|
+
const material = published(row[MATERIAL_COLUMN], what, 'part number');
|
|
130
|
+
return holderRecord({
|
|
131
|
+
brand: familyBrand(family),
|
|
132
|
+
materialNumber: material,
|
|
133
|
+
// MariTool publishes one number per part and no second catalog designation
|
|
134
|
+
// — `conventions.IDENTITY_DEVIATIONS` records why, and inventing one here
|
|
135
|
+
// would put a column in the record the vendor does not publish. The part
|
|
136
|
+
// number is what a human orders by, so it is both.
|
|
137
|
+
catalogNumber: material,
|
|
138
|
+
description: row[DESCRIPTION_COLUMN] ?? '',
|
|
139
|
+
unit,
|
|
140
|
+
// One part in the catalog, `BT40-ER32-60`, publishes no `Taper` cell at all,
|
|
141
|
+
// and `scrape.holderRow` leaves both columns empty rather than inferring one
|
|
142
|
+
// from the part number. It becomes no record and a warning, which is the
|
|
143
|
+
// same call a cutting tool with no overall length gets.
|
|
144
|
+
taper: published(row[TAPER_COLUMN], what, 'taper'),
|
|
145
|
+
contact: contactMode(published(row[CONTACT_COLUMN], what, 'contact mode'), what),
|
|
146
|
+
clamping: clampingMode(published(row[CLAMPING_COLUMN], what, 'clamping mode'), what),
|
|
147
|
+
style: published(row[STYLE_COLUMN], what, 'style'),
|
|
148
|
+
colletSeries: row[COLLET_SERIES_COLUMN] || null,
|
|
149
|
+
bore: diameter(row, SHANK_SIZE_LABEL, unit, what),
|
|
150
|
+
gaugeLength,
|
|
151
|
+
lockNutDiameter: diameter(row, COLLET_NUT_DIAMETER_LABEL, unit, what),
|
|
152
|
+
cadModelUrl: row[CAD_COLUMN] || null,
|
|
153
|
+
cadDxfUrl: row[CAD_DXF_COLUMN] || null,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The toolholding half of the adapter contract `registry` looks up by brand.
|
|
158
|
+
*
|
|
159
|
+
* Holders only: MariTool sells collets and this package does not scrape them,
|
|
160
|
+
* so there is no `collet` mapper and `registry.toHolding` refuses a request for
|
|
161
|
+
* one naming what this brand does map. A partial table is the honest state for
|
|
162
|
+
* a kind nobody has read the columns of.
|
|
163
|
+
*/
|
|
164
|
+
export const HOLDING_MAPPERS = { holder };
|
|
@@ -26,11 +26,16 @@
|
|
|
26
26
|
*
|
|
27
27
|
* ## What the vendor gets wrong
|
|
28
28
|
*
|
|
29
|
-
* Four faults found on 2026-08-29
|
|
29
|
+
* Four faults found on 2026-08-29. Three are reported as warnings rather than
|
|
30
30
|
* fixed — two disagreeing vendor cells cannot say which one is wrong, and a
|
|
31
31
|
* scraper that corrects one becomes a place tool data is authored by hand.
|
|
32
32
|
* This is the same call `vendors/regofix/scrape.ts` made on its three.
|
|
33
33
|
*
|
|
34
|
+
* The fourth is resolved rather than reported, and the difference is where the
|
|
35
|
+
* answer came from: `docs/ADDING-A-VENDOR.md` says that when a vendor label is
|
|
36
|
+
* unclear you **ask**, and record the answer and its date. That is what
|
|
37
|
+
* happened — see fault 2.
|
|
38
|
+
*
|
|
34
39
|
* 1. **`BT40-ER32-60` publishes no `Taper` row at all**, alone among the 529
|
|
35
40
|
* parts in scope. Its row is kept with `taper` and `contact` empty rather
|
|
36
41
|
* than dropped or filled in from its part number: the CSV is a receipt, and
|
|
@@ -38,10 +43,21 @@
|
|
|
38
43
|
* 2. **`Collet Size` carries a collet *nut* designation on two parts.**
|
|
39
44
|
* `CAT40-ER25-3.0MD` and `BT30-ER25-60M` both state `ER25M`, and `ER25M` is
|
|
40
45
|
* not a collet series — `HSK40E-ER16-3.0M` puts exactly that shape of value
|
|
41
|
-
* in its own `Collet Nut` cell, which is the column it belongs in.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
46
|
+
* in its own `Collet Nut` cell, which is the column it belongs in.
|
|
47
|
+
*
|
|
48
|
+
* **`ER25M` is the mini collet nut series, and the collet it closes is a
|
|
49
|
+
* plain ER25** (JG 2026-09-02). So {@link colletSeries} resolves the cell to
|
|
50
|
+
* the collet the holder actually takes, and the two parts join to the ER25
|
|
51
|
+
* collets they fit. Until that answer existed the string was written through
|
|
52
|
+
* as designated and warned about, because widening a series on a guess is
|
|
53
|
+
* how a machinist is offered a collet that does not seat — and the cost of
|
|
54
|
+
* being wrong the other way was one option, not a purchase.
|
|
55
|
+
*
|
|
56
|
+
* **Nothing is lost by resolving it.** `Collet Size` is one of the vendor's
|
|
57
|
+
* own labels and is carried into the CSV verbatim, so the receipt still says
|
|
58
|
+
* `ER25M` and still says which of the two parts has a mini nut. What changes
|
|
59
|
+
* is only `CST`, which is a derived join key rather than a record of what the
|
|
60
|
+
* vendor published — the same thing the spacing rule below does to it.
|
|
45
61
|
* 3. **`Collet Size` is spaced inconsistently within one style** — `ER 11`
|
|
46
62
|
* and `ER11` are both published. {@link colletSeries} closes the space,
|
|
47
63
|
* because `CST` is a join key and two spellings of one series join to
|
|
@@ -254,15 +270,23 @@ export declare function parseGageLength(cell: string): GageLength;
|
|
|
254
270
|
/**
|
|
255
271
|
* A `Collet Size` cell as the series `CST` joins a collet family on.
|
|
256
272
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
273
|
+
* **Derived, not a receipt.** `CST` is the key `families/kennametal.ts` states
|
|
274
|
+
* the holder-to-collet join against, so what belongs in it is the series of the
|
|
275
|
+
* collet the holder takes. The vendor's own cell is carried into the CSV under
|
|
276
|
+
* its own label and untouched, which is where the two normalisations below are
|
|
277
|
+
* still visible.
|
|
278
|
+
*
|
|
279
|
+
* Two things are closed, and both exist because two spellings of one series
|
|
280
|
+
* join to nothing:
|
|
281
|
+
*
|
|
282
|
+
* - **Spacing.** MariTool publishes `ER 11` and `ER11` within one style, and
|
|
283
|
+
* the collet side of the join spells the series without a space.
|
|
284
|
+
* - **A nut designation.** `ER25M` names a mini nut, not a collet series — see
|
|
285
|
+
* {@link NUT_DESIGNATION} and this module's docstring.
|
|
261
286
|
*
|
|
262
|
-
* Nothing else is
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* carries elsewhere — see this module's docstring. {@link holderRow} warns.
|
|
287
|
+
* Nothing else is. A cell this leaves as neither `ER<n>` nor a nut designation
|
|
288
|
+
* is written through as the vendor designated it, and {@link holderRow} warns
|
|
289
|
+
* that it joins to no collet.
|
|
266
290
|
*/
|
|
267
291
|
export declare function colletSeries(cell: string): string;
|
|
268
292
|
/**
|