@softwear/latestcollectioncore 1.0.203 → 1.0.205
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/imageBinder.d.ts +1 -1
- package/dist/imageBinder.js +253 -77
- package/dist/types.d.ts +0 -1
- package/package.json +1 -1
- package/src/imageBinder.ts +245 -79
- package/src/types.ts +0 -1
- package/test/edifact-primadonna.json +0 -19
- package/test/imageBinder.spec.js +52 -3
- package/test/imageBinderTestData/brandDataMatched/exxcellent.json +59633 -0
- package/test/imageBinderTestData/brandDataMatched/lingadore.json +16752 -0
- package/test/imageBinderTestData/brandDataMatched/pennAndInk.json +71524 -0
- package/test/imageBinderTestData/brandImages/exxcellent.json +491 -0
- package/test/imageBinderTestData/brandImages/lingadore.json +47 -0
- package/test/imageBinderTestData/brandImages/pennAndInk.json +511 -0
- package/test/imageBinderTestData/brandSkus/exxcellent.json +24453 -0
- package/test/imageBinderTestData/brandSkus/lingadore.json +5102 -0
- package/test/imageBinderTestData/brandSkus/pennAndInk.json +280186 -0
package/src/imageBinder.ts
CHANGED
|
@@ -25,11 +25,89 @@ const GLOBAL_PATTERNS = [
|
|
|
25
25
|
['articleDescription', 'articleCode|articleCodeSupplier', 'colorCodeSupplier|colorSupplier'],
|
|
26
26
|
['colorCodeSupplier|colorSupplier', 'articleCode|articleCodeSupplier'],
|
|
27
27
|
['brand', 'collectionSupplier', 'colorCodeSupplier', 'articleCode|articleCodeSupplier'],
|
|
28
|
-
['brand', 'collectionSupplier', 'colorCodeSupplier', 'articleCode|articleCodeSupplier', 'buyPrice|valuePrice'],
|
|
29
28
|
['brand', 'articleDescription', 'colorSupplier'],
|
|
30
29
|
]
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
const FIELDS_TO_SKIP: Array<string> = ['__created', '__modified', 'images', 'brandhash', 'active', 'PRODUCT', 'colorFamily', 'price', 'sizeIndex', '_customsize', 'SELECTED']
|
|
32
|
+
|
|
33
|
+
const MERGE_GROUPS: Record<string, string> = {
|
|
34
|
+
'id|barcode': 'id',
|
|
35
|
+
'articleCode|articleCodeSupplier': 'articleCode',
|
|
36
|
+
'colorCode|colorCodeSupplier|colorSupplier': 'color',
|
|
37
|
+
'size|sizeSupplier|mainSize|subSize|subSizeSupplier|_customsize': 'size',
|
|
38
|
+
'articleGroup|articleGroupSupplier': 'articleGroup',
|
|
39
|
+
'collection|collectionSupplier': 'collection',
|
|
40
|
+
'brand|brandSupplier': 'brand',
|
|
41
|
+
'_gender|genderSupplier': 'gender',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Letters, digits, and apostrophe stay; everything else is a delimiter.
|
|
45
|
+
const DELIMITERS = /[^\p{L}\p{N}']+/gu
|
|
46
|
+
|
|
47
|
+
/** Shared stem: decode URI, then strip from the first known image extension onward. */
|
|
48
|
+
function filenameStem(fileName: string): string {
|
|
49
|
+
let s: string
|
|
50
|
+
try {
|
|
51
|
+
s = decodeURIComponent(fileName)
|
|
52
|
+
} catch {
|
|
53
|
+
s = fileName
|
|
54
|
+
}
|
|
55
|
+
s = s.toLowerCase()
|
|
56
|
+
const extIdx = s.search(/\.(?:jpe?g|png|gif)/i)
|
|
57
|
+
if (extIdx !== -1) s = s.slice(0, extIdx)
|
|
58
|
+
return s
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function checkPattern(fieldVals: Array<string>, fileName: string) {
|
|
62
|
+
|
|
63
|
+
const fieldIndexes2: Record<string, Array<number>> = {}
|
|
64
|
+
|
|
65
|
+
const sortedFields = fieldVals.sort((a, b) => {
|
|
66
|
+
return b.length - a.length
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Return quickly if one of the fields in the pattern is not in the fileName
|
|
70
|
+
for (const fieldVal of sortedFields) {
|
|
71
|
+
const idx = fileName.indexOf(fieldVal)
|
|
72
|
+
if (idx == -1) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
fieldIndexes2[fieldVal] = [idx, idx+fieldVal.length]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if all fields have a specific place in file name, with no overlap
|
|
79
|
+
for (let i = 1; i < sortedFields.length; i++) {
|
|
80
|
+
const startIndex = fieldIndexes2[sortedFields[i]][0]
|
|
81
|
+
const endIndex = fieldIndexes2[sortedFields[i]][1]
|
|
82
|
+
for (let j = 0; j < i; j++) {
|
|
83
|
+
const startIndex2 = fieldIndexes2[sortedFields[j]][0]
|
|
84
|
+
const endIndex2 = fieldIndexes2[sortedFields[j]][1]
|
|
85
|
+
if (startIndex < endIndex2 && endIndex > startIndex2) {
|
|
86
|
+
const nextIndex = fileName.indexOf(sortedFields[i], endIndex2)
|
|
87
|
+
if (nextIndex == -1) {
|
|
88
|
+
return false
|
|
89
|
+
} else {
|
|
90
|
+
fieldIndexes2[sortedFields[i]] = [nextIndex, nextIndex+sortedFields[i].length]
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeSkuField(sku: SkuI, fieldName: string) {
|
|
101
|
+
let fieldVal = sku[fieldName]
|
|
102
|
+
if (fieldVal == null || fieldVal == '') return ''
|
|
103
|
+
if (typeof fieldVal == 'string') {
|
|
104
|
+
return fieldVal.replace(DELIMITERS, '').toLowerCase()
|
|
105
|
+
} else {
|
|
106
|
+
return fieldVal.toString()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function prepFilterSkusImagesAuto(filteredSkus: SkuI[], fileNames: Array<string>) {
|
|
33
111
|
const skusCopy = deepCopy(filteredSkus)
|
|
34
112
|
const preppedSkus: Array<Record<string, any>> = []
|
|
35
113
|
const fileNames2skip: Array<string> = []
|
|
@@ -47,41 +125,92 @@ function prepFilterSkusImages(filteredSkus: SkuI[], merge = true, fileNames: Arr
|
|
|
47
125
|
|
|
48
126
|
const fieldsToBeMerged = {}
|
|
49
127
|
for (const fieldName in sku) {
|
|
50
|
-
if (fieldName
|
|
51
|
-
|
|
52
|
-
|
|
128
|
+
if (FIELDS_TO_SKIP.indexOf(fieldName) != -1 || fieldName.includes('Price')) continue
|
|
129
|
+
if (!(fieldName in sku) || sku[fieldName] == null || sku[fieldName] == undefined) continue
|
|
130
|
+
sku[fieldName] = normalizeSkuField(sku, fieldName)
|
|
53
131
|
|
|
54
|
-
|
|
55
|
-
|
|
132
|
+
const fieldVal = sku[fieldName]
|
|
133
|
+
if (!fieldVal) continue
|
|
134
|
+
|
|
135
|
+
if (fieldVal in fieldsToBeMerged) {
|
|
136
|
+
fieldsToBeMerged[fieldVal].push(fieldName)
|
|
56
137
|
} else {
|
|
57
|
-
|
|
138
|
+
fieldsToBeMerged[fieldVal] = [fieldName]
|
|
58
139
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
140
|
+
}
|
|
141
|
+
const newSku = {}
|
|
142
|
+
newSku['originalSku'] = filteredSkus[idx]
|
|
143
|
+
for (const fieldVal in fieldsToBeMerged) {
|
|
144
|
+
const fieldNameArray = fieldsToBeMerged[fieldVal].sort()
|
|
145
|
+
if (fieldNameArray.length > 1) {
|
|
146
|
+
const groups = {}
|
|
147
|
+
for (const fieldName of fieldNameArray) {
|
|
148
|
+
let included = false
|
|
149
|
+
Object.keys(MERGE_GROUPS).forEach((group) => {
|
|
150
|
+
if (group.includes(fieldName)) {
|
|
151
|
+
included = true
|
|
152
|
+
if (!groups[MERGE_GROUPS[group]]) groups[MERGE_GROUPS[group]] = fieldName
|
|
153
|
+
else groups[MERGE_GROUPS[group]] = groups[MERGE_GROUPS[group]] + '|' + fieldName
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
if (!included) {
|
|
157
|
+
groups[fieldName] = fieldName
|
|
158
|
+
}
|
|
67
159
|
}
|
|
160
|
+
Object.keys(groups).forEach((group) => {
|
|
161
|
+
newSku[groups[group]] = fieldVal
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
newSku[fieldNameArray[0]] = fieldVal
|
|
68
165
|
}
|
|
69
166
|
}
|
|
70
|
-
|
|
167
|
+
preppedSkus.push(newSku)
|
|
168
|
+
})
|
|
169
|
+
const fileNames2Match = fileNames.filter((fileName) => {
|
|
170
|
+
return fileNames2skip.indexOf(fileName) == -1
|
|
171
|
+
})
|
|
172
|
+
return { preppedSkus: preppedSkus, fileNames2Match: fileNames2Match }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function prepFilterSkusImagesManual(filteredSkus: SkuI[], fileNames: Array<string>, pattern: Array<string>) {
|
|
176
|
+
const skusCopy = deepCopy(filteredSkus)
|
|
177
|
+
const preppedSkus: Array<Record<string, any>> = []
|
|
178
|
+
const fileNames2skip: Array<string> = []
|
|
179
|
+
skusCopy.forEach((sku, idx) => {
|
|
180
|
+
if (sku.images && sku.images.length > 0) {
|
|
181
|
+
// If a fileName is already matched, we skip it
|
|
182
|
+
fileNames.forEach((fileName) => {
|
|
183
|
+
if (sku.images?.includes(fileName) && fileNames2skip.indexOf(fileName) == -1) fileNames2skip.push(fileName)
|
|
184
|
+
})
|
|
185
|
+
// If a sku has images, we skip it
|
|
186
|
+
sku['skip'] = true
|
|
71
187
|
preppedSkus.push(sku)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const foundValues: Array<string> = []
|
|
192
|
+
for (const fieldName of pattern) {
|
|
193
|
+
if (!(fieldName in sku) || sku[fieldName] == null || sku[fieldName] == undefined) {
|
|
194
|
+
sku['skip'] = true
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
sku[fieldName] = normalizeSkuField(sku, fieldName)
|
|
198
|
+
if (!sku[fieldName]) {
|
|
199
|
+
sku['skip'] = true
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
for (const value of foundValues) {
|
|
203
|
+
if (value.includes(sku[fieldName]) || sku[fieldName] == value) {
|
|
204
|
+
if (!sku['subStrings']) sku['subStrings'] = []
|
|
205
|
+
sku['subStrings'].push([value, sku[fieldName]])
|
|
206
|
+
} else if (sku[fieldName].includes(value)) {
|
|
207
|
+
if (!sku['subStrings']) sku['subStrings'] = []
|
|
208
|
+
sku['subStrings'].push([sku[fieldName], value])
|
|
81
209
|
}
|
|
82
210
|
}
|
|
83
|
-
|
|
211
|
+
foundValues.push(sku[fieldName])
|
|
84
212
|
}
|
|
213
|
+
preppedSkus.push(sku)
|
|
85
214
|
})
|
|
86
215
|
const fileNames2Match = fileNames.filter((fileName) => {
|
|
87
216
|
return fileNames2skip.indexOf(fileName) == -1
|
|
@@ -90,19 +219,10 @@ function prepFilterSkusImages(filteredSkus: SkuI[], merge = true, fileNames: Arr
|
|
|
90
219
|
}
|
|
91
220
|
|
|
92
221
|
function cleanFilename(fileName: string) {
|
|
93
|
-
return (
|
|
94
|
-
fileName
|
|
95
|
-
// ── 1. strip any query string (everything from the first “?” to the end)
|
|
96
|
-
.replace(/\?.*$/, '')
|
|
97
|
-
// ── 2. strip a terminal image extension (case‑insensitive) .jpeg, .jpg, .png or .gif
|
|
98
|
-
.replace(/\.(?:jpe?g|png|gif)$/i, '')
|
|
99
|
-
// ── 3. drop every character that is NOT a letter (Unicode!) or a digit
|
|
100
|
-
.replace(/[^\p{L}\p{N}]+/gu, '')
|
|
101
|
-
.toLowerCase()
|
|
102
|
-
)
|
|
222
|
+
return filenameStem(fileName).replace(DELIMITERS, '')
|
|
103
223
|
}
|
|
104
224
|
|
|
105
|
-
function
|
|
225
|
+
function removeLastToken(fileName: string) {
|
|
106
226
|
// If last token equals any of the following patterns, we will check the fileName without the last token
|
|
107
227
|
// 1) includes front, back, side or sole
|
|
108
228
|
// 2) is 1 alphanumerical character
|
|
@@ -112,11 +232,9 @@ function checkWithoutLast(fileName: string) {
|
|
|
112
232
|
// 5) ends with a number
|
|
113
233
|
// 6) ends with two numbers
|
|
114
234
|
// If any of those checks are true, we return the fileName without the last token
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.split(/[_\-.\s()]+|%[0-9A-Fa-f]{2}/)
|
|
119
|
-
.slice(0, -1)
|
|
235
|
+
// Tokens come from the shared stem (decode + extension strip), split on anything that is
|
|
236
|
+
// not a letter, digit, or apostrophe — same rule as cleanFilename.
|
|
237
|
+
const tokens = filenameStem(fileName).split(DELIMITERS).filter(Boolean)
|
|
120
238
|
const lastToken = tokens[tokens.length - 1]
|
|
121
239
|
let returnWithoutLast = false
|
|
122
240
|
if (lastToken) {
|
|
@@ -127,37 +245,22 @@ function checkWithoutLast(fileName: string) {
|
|
|
127
245
|
const tokenLength = tokens.length
|
|
128
246
|
if (returnWithoutLast) {
|
|
129
247
|
let sliceIndex = -1
|
|
130
|
-
if (tokenLength > 3 && tokens[tokenLength - 3] == 'press' && (
|
|
248
|
+
if (tokenLength > 3 && tokens[tokenLength - 3] == 'press' && (tokens[tokenLength - 2] == 'pack' || tokens[tokenLength - 2] == 'model')) sliceIndex = -3
|
|
131
249
|
else if (tokenLength > 2 && tokens[tokenLength - 2] == 'packshot') sliceIndex = -2
|
|
132
|
-
return tokens
|
|
133
|
-
.slice(0, sliceIndex)
|
|
134
|
-
.join('')
|
|
135
|
-
.replace(/[^\p{L}\p{N}]+/gu, '')
|
|
250
|
+
return tokens.slice(0, sliceIndex).join('')
|
|
136
251
|
}
|
|
137
|
-
if (tokenLength > 2 && tokens[tokenLength - 2] == 'press' && (
|
|
138
|
-
return tokens
|
|
139
|
-
.slice(0, -2)
|
|
140
|
-
.join('')
|
|
141
|
-
.replace(/[^\p{L}\p{N}]+/gu, '')
|
|
252
|
+
if (tokenLength > 2 && tokens[tokenLength - 2] == 'press' && (tokens[tokenLength - 1] == 'pack' || tokens[tokenLength - 1] == 'model')) {
|
|
253
|
+
return tokens.slice(0, -2).join('')
|
|
142
254
|
} else if (tokenLength > 1 && tokens[tokenLength - 1] == 'packshot') {
|
|
143
|
-
return tokens
|
|
144
|
-
.slice(0, -1)
|
|
145
|
-
.join('')
|
|
146
|
-
.replace(/[^\p{L}\p{N}]+/gu, '')
|
|
255
|
+
return tokens.slice(0, -1).join('')
|
|
147
256
|
}
|
|
148
257
|
if (lastToken.includes('product')) {
|
|
149
|
-
const joinedCleanFileName = tokens.join('')
|
|
258
|
+
const joinedCleanFileName = tokens.join('')
|
|
150
259
|
return joinedCleanFileName.slice(0, joinedCleanFileName.indexOf('product'))
|
|
151
260
|
} else if (Number(lastToken.slice(-1))) {
|
|
152
|
-
return tokens
|
|
153
|
-
.join('')
|
|
154
|
-
.replace(/[^\p{L}\p{N}]+/gu, '')
|
|
155
|
-
.slice(0, -1)
|
|
261
|
+
return tokens.join('').slice(0, -1)
|
|
156
262
|
} else if (Number(lastToken.slice(-2))) {
|
|
157
|
-
return tokens
|
|
158
|
-
.join('')
|
|
159
|
-
.replace(/[^\p{L}\p{N}]+/gu, '')
|
|
160
|
-
.substring(0, -2)
|
|
263
|
+
return tokens.join('').substring(0, -2)
|
|
161
264
|
}
|
|
162
265
|
}
|
|
163
266
|
return ''
|
|
@@ -364,7 +467,6 @@ function fallbackSearch(cleanedFileName: string, fileNameWithoutLast: string, pr
|
|
|
364
467
|
}
|
|
365
468
|
|
|
366
469
|
function imageBinder(fileNames: Array<string>, filteredSkus: SkuI[], brand: string, specifiedPattern: Array<string>, test = false) {
|
|
367
|
-
const totalLength = fileNames.length
|
|
368
470
|
let count = 1
|
|
369
471
|
const fileNameIndexMatch = {}
|
|
370
472
|
let quickCount = 0
|
|
@@ -373,32 +475,96 @@ function imageBinder(fileNames: Array<string>, filteredSkus: SkuI[], brand: stri
|
|
|
373
475
|
let newPatterns = {}
|
|
374
476
|
if (specifiedPattern[0] != 'auto') {
|
|
375
477
|
// If the user/administrator provides a pattern, we use that by default
|
|
376
|
-
const prepObject =
|
|
478
|
+
const prepObject = prepFilterSkusImagesManual(filteredSkus, fileNames, specifiedPattern)
|
|
377
479
|
const preppedSkus = prepObject.preppedSkus
|
|
378
480
|
const fileNames2Match = prepObject.fileNames2Match
|
|
379
481
|
fileNames2Match.forEach((fileName) => {
|
|
380
482
|
const cleanedFileName = cleanFilename(fileName)
|
|
381
|
-
const fileNameWithoutLast = checkWithoutLast(fileName)
|
|
382
483
|
fileNameIndexMatch[fileName] = []
|
|
484
|
+
const matchingCombinations = {}
|
|
485
|
+
// Check which skus match the specified pattern and store matching fields (combinations)
|
|
383
486
|
preppedSkus.forEach((sku, idx) => {
|
|
384
487
|
if (sku['skip']) return
|
|
385
488
|
let skuStillMatches = true
|
|
489
|
+
let combinationArray: Array<string> = []
|
|
386
490
|
specifiedPattern.every((fieldName) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (!cleanedFileName.includes(fieldVal) && !fileNameWithoutLast.includes(fieldVal)) skuStillMatches = false
|
|
391
|
-
}
|
|
491
|
+
const fieldVal = sku[fieldName]
|
|
492
|
+
if (!cleanedFileName.includes(fieldVal)) skuStillMatches = false
|
|
493
|
+
else combinationArray.push(fieldVal)
|
|
392
494
|
return skuStillMatches
|
|
393
495
|
})
|
|
394
496
|
if (skuStillMatches) {
|
|
395
|
-
|
|
497
|
+
let combination = combinationArray.join('|')
|
|
498
|
+
if (combination in matchingCombinations) {
|
|
499
|
+
matchingCombinations[combination].push(idx)
|
|
500
|
+
} else {
|
|
501
|
+
matchingCombinations[combination] = [idx]
|
|
502
|
+
}
|
|
396
503
|
}
|
|
397
504
|
})
|
|
505
|
+
let matchingIndices: Array<number> = []
|
|
506
|
+
// If only one combination of specified pattern matches, we use it
|
|
507
|
+
if (Object.keys(matchingCombinations).length == 1) {
|
|
508
|
+
matchingIndices = matchingCombinations[Object.keys(matchingCombinations)[0]] }
|
|
509
|
+
else {
|
|
510
|
+
// In case of multiple combinations, do substring check
|
|
511
|
+
// -> Checks if all fields have a specific place in file name, with no overlap
|
|
512
|
+
const matchesAfterSubstringCheck = {}
|
|
513
|
+
Object.keys(matchingCombinations).forEach((combination) => {
|
|
514
|
+
const combinationArray = combination.split('|')
|
|
515
|
+
const isMatch = checkPattern(combinationArray, cleanedFileName)
|
|
516
|
+
if (isMatch) {
|
|
517
|
+
matchesAfterSubstringCheck[combination] = matchingCombinations[combination]
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
// If only one combination is left after substring check, we use it
|
|
521
|
+
if (Object.keys(matchesAfterSubstringCheck).length == 1) {
|
|
522
|
+
matchingIndices = matchesAfterSubstringCheck[Object.keys(matchesAfterSubstringCheck)[0]] }
|
|
523
|
+
else {
|
|
524
|
+
// In case there are still multiple combinations, we check longest combinations
|
|
525
|
+
// -> Longest combinations are more specific and therefore preferred
|
|
526
|
+
let lengthLongest = 0
|
|
527
|
+
let longestCombinations: Array<string> = []
|
|
528
|
+
Object.keys(matchesAfterSubstringCheck).forEach((combination) => {
|
|
529
|
+
if (combination.length > lengthLongest) {
|
|
530
|
+
lengthLongest = combination.length
|
|
531
|
+
longestCombinations = [combination]
|
|
532
|
+
} else if (combination.length == lengthLongest) {
|
|
533
|
+
longestCombinations.push(combination)
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
// If only one combination is left after longest combination check, we use it
|
|
537
|
+
if (longestCombinations.length == 1) {
|
|
538
|
+
matchingIndices = matchesAfterSubstringCheck[longestCombinations[0]] }
|
|
539
|
+
else {
|
|
540
|
+
// In case there are still multiple combinations, we check fileNameWithoutLast
|
|
541
|
+
// -> If any combination matches fileNameWithoutLast, we prefer it, as last tokens can be junk
|
|
542
|
+
const fileNameWithoutLast = removeLastToken(fileName)
|
|
543
|
+
const matchesWithoutLast: Record<string, number[]> = {}
|
|
544
|
+
if (fileNameWithoutLast) {
|
|
545
|
+
longestCombinations.forEach((combination) => {
|
|
546
|
+
const combinationArray = combination.split('|')
|
|
547
|
+
if (checkPattern(combinationArray, fileNameWithoutLast)) {
|
|
548
|
+
matchesWithoutLast[combination] = matchesAfterSubstringCheck[combination]
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
// If only one combination is left after fileNameWithoutLast check, we use it
|
|
553
|
+
if (Object.keys(matchesWithoutLast).length == 1) { matchingIndices = matchesWithoutLast[Object.keys(matchesWithoutLast)[0]] }
|
|
554
|
+
else {
|
|
555
|
+
Object.keys(matchesAfterSubstringCheck).forEach((combination) => {
|
|
556
|
+
matchingIndices.push(...matchesAfterSubstringCheck[combination])
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (matchingIndices.length > 0) fileNameIndexMatch[fileName] = matchingIndices
|
|
563
|
+
if (!fileNameIndexMatch[fileName]) fileNameIndexMatch[fileName] = []
|
|
398
564
|
})
|
|
399
565
|
} else {
|
|
400
566
|
// Else, we look for patterns ourselves using a couple of methods, prioritized on efficiency
|
|
401
|
-
const prepObject =
|
|
567
|
+
const prepObject = prepFilterSkusImagesAuto(filteredSkus, fileNames)
|
|
402
568
|
const preppedSkus = prepObject.preppedSkus
|
|
403
569
|
const fileNames2Match = prepObject.fileNames2Match
|
|
404
570
|
let quickSearch = prepQuickSearch(preppedSkus)
|
|
@@ -406,7 +572,7 @@ function imageBinder(fileNames: Array<string>, filteredSkus: SkuI[], brand: stri
|
|
|
406
572
|
fileNames2Match.forEach((fileName) => {
|
|
407
573
|
let matchFound = false
|
|
408
574
|
const cleanedFileName = cleanFilename(fileName)
|
|
409
|
-
const fileNameWithoutLast =
|
|
575
|
+
const fileNameWithoutLast = removeLastToken(fileName)
|
|
410
576
|
let indicesForFileName: Array<number> = []
|
|
411
577
|
// Method #1: try quicksearch, an object with patterns as keys and arrays of [proposedFilename, sku] as values
|
|
412
578
|
// based on existing patterns in GLOBAL_PATTERNS
|
|
@@ -469,7 +635,7 @@ function onlySaveIDFromSkus(matchDict: {}): Record<string, number[]> {
|
|
|
469
635
|
return out
|
|
470
636
|
}
|
|
471
637
|
|
|
472
|
-
async function testBatch(brand: string) {
|
|
638
|
+
async function testBatch(brand: string, specifiedPattern: Array<string> = ['auto']) {
|
|
473
639
|
const [skusModule, imagesModule, matchedModule] = await Promise.all([
|
|
474
640
|
import(`../test/imageBinderTestData/brandSkus/${brand}.json`),
|
|
475
641
|
import(`../test/imageBinderTestData/brandImages/${brand}.json`),
|
|
@@ -482,7 +648,7 @@ async function testBatch(brand: string) {
|
|
|
482
648
|
|
|
483
649
|
// 1) Call imageBinder
|
|
484
650
|
const t0 = Date.now()
|
|
485
|
-
const imageBinderResult = imageBinder(fileNames, filteredSkus, brand,
|
|
651
|
+
const imageBinderResult = imageBinder(fileNames, filteredSkus, brand, specifiedPattern, true)
|
|
486
652
|
const t1 = Date.now()
|
|
487
653
|
const matchIndices = imageBinderResult.match || {}
|
|
488
654
|
|
package/src/types.ts
CHANGED
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"source": "latestCollection",
|
|
26
26
|
"mainColorCode": "",
|
|
27
27
|
"mainColorDescription": "",
|
|
28
|
-
"skuActive": true,
|
|
29
28
|
"valuePrice": 37.66,
|
|
30
29
|
"supplierName": "",
|
|
31
30
|
"sizeSupplier": "75C",
|
|
@@ -63,7 +62,6 @@
|
|
|
63
62
|
"source": "latestCollection",
|
|
64
63
|
"mainColorCode": "",
|
|
65
64
|
"mainColorDescription": "",
|
|
66
|
-
"skuActive": true,
|
|
67
65
|
"valuePrice": 37.66,
|
|
68
66
|
"supplierName": "",
|
|
69
67
|
"sizeSupplier": "80C",
|
|
@@ -101,7 +99,6 @@
|
|
|
101
99
|
"source": "latestCollection",
|
|
102
100
|
"mainColorCode": "",
|
|
103
101
|
"mainColorDescription": "",
|
|
104
|
-
"skuActive": true,
|
|
105
102
|
"valuePrice": 37.66,
|
|
106
103
|
"supplierName": "",
|
|
107
104
|
"sizeSupplier": "85C",
|
|
@@ -139,7 +136,6 @@
|
|
|
139
136
|
"source": "latestCollection",
|
|
140
137
|
"mainColorCode": "",
|
|
141
138
|
"mainColorDescription": "",
|
|
142
|
-
"skuActive": true,
|
|
143
139
|
"valuePrice": 37.66,
|
|
144
140
|
"supplierName": "",
|
|
145
141
|
"sizeSupplier": "90C",
|
|
@@ -177,7 +173,6 @@
|
|
|
177
173
|
"source": "latestCollection",
|
|
178
174
|
"mainColorCode": "",
|
|
179
175
|
"mainColorDescription": "",
|
|
180
|
-
"skuActive": true,
|
|
181
176
|
"valuePrice": 37.66,
|
|
182
177
|
"supplierName": "",
|
|
183
178
|
"sizeSupplier": "95C",
|
|
@@ -215,7 +210,6 @@
|
|
|
215
210
|
"source": "latestCollection",
|
|
216
211
|
"mainColorCode": "",
|
|
217
212
|
"mainColorDescription": "",
|
|
218
|
-
"skuActive": true,
|
|
219
213
|
"valuePrice": 37.66,
|
|
220
214
|
"supplierName": "",
|
|
221
215
|
"sizeSupplier": "100C",
|
|
@@ -253,7 +247,6 @@
|
|
|
253
247
|
"source": "latestCollection",
|
|
254
248
|
"mainColorCode": "",
|
|
255
249
|
"mainColorDescription": "",
|
|
256
|
-
"skuActive": true,
|
|
257
250
|
"valuePrice": 37.66,
|
|
258
251
|
"supplierName": "",
|
|
259
252
|
"sizeSupplier": "105C",
|
|
@@ -291,7 +284,6 @@
|
|
|
291
284
|
"source": "latestCollection",
|
|
292
285
|
"mainColorCode": "",
|
|
293
286
|
"mainColorDescription": "",
|
|
294
|
-
"skuActive": true,
|
|
295
287
|
"valuePrice": 37.66,
|
|
296
288
|
"supplierName": "",
|
|
297
289
|
"sizeSupplier": "75C",
|
|
@@ -329,7 +321,6 @@
|
|
|
329
321
|
"source": "latestCollection",
|
|
330
322
|
"mainColorCode": "",
|
|
331
323
|
"mainColorDescription": "",
|
|
332
|
-
"skuActive": true,
|
|
333
324
|
"valuePrice": 37.66,
|
|
334
325
|
"supplierName": "",
|
|
335
326
|
"sizeSupplier": "80C",
|
|
@@ -367,7 +358,6 @@
|
|
|
367
358
|
"source": "latestCollection",
|
|
368
359
|
"mainColorCode": "",
|
|
369
360
|
"mainColorDescription": "",
|
|
370
|
-
"skuActive": true,
|
|
371
361
|
"valuePrice": 37.66,
|
|
372
362
|
"supplierName": "",
|
|
373
363
|
"sizeSupplier": "85C",
|
|
@@ -405,7 +395,6 @@
|
|
|
405
395
|
"source": "latestCollection",
|
|
406
396
|
"mainColorCode": "",
|
|
407
397
|
"mainColorDescription": "",
|
|
408
|
-
"skuActive": true,
|
|
409
398
|
"valuePrice": 37.66,
|
|
410
399
|
"supplierName": "",
|
|
411
400
|
"sizeSupplier": "90C",
|
|
@@ -443,7 +432,6 @@
|
|
|
443
432
|
"source": "latestCollection",
|
|
444
433
|
"mainColorCode": "",
|
|
445
434
|
"mainColorDescription": "",
|
|
446
|
-
"skuActive": true,
|
|
447
435
|
"valuePrice": 37.66,
|
|
448
436
|
"supplierName": "",
|
|
449
437
|
"sizeSupplier": "95C",
|
|
@@ -481,7 +469,6 @@
|
|
|
481
469
|
"source": "latestCollection",
|
|
482
470
|
"mainColorCode": "",
|
|
483
471
|
"mainColorDescription": "",
|
|
484
|
-
"skuActive": true,
|
|
485
472
|
"valuePrice": 18.81,
|
|
486
473
|
"supplierName": "",
|
|
487
474
|
"sizeSupplier": "40",
|
|
@@ -518,7 +505,6 @@
|
|
|
518
505
|
"source": "latestCollection",
|
|
519
506
|
"mainColorCode": "",
|
|
520
507
|
"mainColorDescription": "",
|
|
521
|
-
"skuActive": true,
|
|
522
508
|
"valuePrice": 18.81,
|
|
523
509
|
"supplierName": "",
|
|
524
510
|
"sizeSupplier": "42",
|
|
@@ -555,7 +541,6 @@
|
|
|
555
541
|
"source": "latestCollection",
|
|
556
542
|
"mainColorCode": "",
|
|
557
543
|
"mainColorDescription": "",
|
|
558
|
-
"skuActive": true,
|
|
559
544
|
"valuePrice": 18.81,
|
|
560
545
|
"supplierName": "",
|
|
561
546
|
"sizeSupplier": "44",
|
|
@@ -592,7 +577,6 @@
|
|
|
592
577
|
"source": "latestCollection",
|
|
593
578
|
"mainColorCode": "",
|
|
594
579
|
"mainColorDescription": "",
|
|
595
|
-
"skuActive": true,
|
|
596
580
|
"valuePrice": 18.81,
|
|
597
581
|
"supplierName": "",
|
|
598
582
|
"sizeSupplier": "46",
|
|
@@ -629,7 +613,6 @@
|
|
|
629
613
|
"source": "latestCollection",
|
|
630
614
|
"mainColorCode": "",
|
|
631
615
|
"mainColorDescription": "",
|
|
632
|
-
"skuActive": true,
|
|
633
616
|
"valuePrice": 18.81,
|
|
634
617
|
"supplierName": "",
|
|
635
618
|
"sizeSupplier": "48",
|
|
@@ -666,7 +649,6 @@
|
|
|
666
649
|
"source": "latestCollection",
|
|
667
650
|
"mainColorCode": "",
|
|
668
651
|
"mainColorDescription": "",
|
|
669
|
-
"skuActive": true,
|
|
670
652
|
"valuePrice": 18.81,
|
|
671
653
|
"supplierName": "",
|
|
672
654
|
"sizeSupplier": "50",
|
|
@@ -703,7 +685,6 @@
|
|
|
703
685
|
"source": "latestCollection",
|
|
704
686
|
"mainColorCode": "",
|
|
705
687
|
"mainColorDescription": "",
|
|
706
|
-
"skuActive": true,
|
|
707
688
|
"valuePrice": 18.81,
|
|
708
689
|
"supplierName": "",
|
|
709
690
|
"sizeSupplier": "52",
|
package/test/imageBinder.spec.js
CHANGED
|
@@ -15,12 +15,12 @@ describe('imageBinder function', function () {
|
|
|
15
15
|
expect(result.Time).toBeLessThan(10)
|
|
16
16
|
}, 300000)
|
|
17
17
|
it('should deal with large batches', async function () {
|
|
18
|
-
// Tests on 1594 images and 18196 skus
|
|
18
|
+
// Tests on 1594 images and 18196 skus; most are quick search
|
|
19
19
|
const result = await testBatch('lowa')
|
|
20
20
|
expect(Object.keys(result).length).toBe(8)
|
|
21
21
|
expect(result.Mismatches).toBe(0)
|
|
22
|
-
expect(result.QuickSearch).toBe(
|
|
23
|
-
expect(result.Patterns['articleCode|articleCodeSupplier-colorCodeSupplier|colorSupplier']).toBe(
|
|
22
|
+
expect(result.QuickSearch).toBe(1588)
|
|
23
|
+
expect(result.Patterns['articleCode|articleCodeSupplier-colorCodeSupplier|colorSupplier']).toBe(1588)
|
|
24
24
|
// Time can vary, so we use a loose check (within 5 seconds)
|
|
25
25
|
expect(result.Time).toBeGreaterThan(0)
|
|
26
26
|
expect(result.Time).toBeLessThan(5)
|
|
@@ -48,4 +48,53 @@ describe('imageBinder function', function () {
|
|
|
48
48
|
expect(result.Time).toBeGreaterThan(0)
|
|
49
49
|
expect(result.Time).toBeLessThan(5)
|
|
50
50
|
})
|
|
51
|
+
it('should prefer the longest matching combination for specified patterns (lingadore 8501)', async function () {
|
|
52
|
+
// When articleCodeSupplier values share prefixes (8501 vs 8501B vs 8501BD),
|
|
53
|
+
// images must match only the most specific article code.
|
|
54
|
+
const result = await testBatch('lingadore', ['articleCodeSupplier', 'colorCodeSupplier'])
|
|
55
|
+
expect(Object.keys(result).length).toBe(8)
|
|
56
|
+
expect(result.Brand).toBe('lingadore')
|
|
57
|
+
expect(result.Mismatches).toBe(0)
|
|
58
|
+
expect(result.QuickSearch).toBe(0)
|
|
59
|
+
expect(result.FallbackSearch).toBe(0)
|
|
60
|
+
expect(result.Time).toBeGreaterThan(0)
|
|
61
|
+
expect(result.Time).toBeLessThan(5)
|
|
62
|
+
})
|
|
63
|
+
it('should not match if substrings do not appear in separate positions (pennAndInk)', async function () {
|
|
64
|
+
// e.g. article W26B422 with colors 15/90 must not also match color 42 just because "42"
|
|
65
|
+
// appears inside the article code.
|
|
66
|
+
const result = await testBatch('pennAndInk', ['articleCodeSupplier', 'colorCodeSupplier'])
|
|
67
|
+
expect(Object.keys(result).length).toBe(8)
|
|
68
|
+
expect(result.Brand).toBe('pennAndInk')
|
|
69
|
+
expect(result.Mismatches).toBe(0)
|
|
70
|
+
expect(result.QuickSearch).toBe(0)
|
|
71
|
+
expect(result.FallbackSearch).toBe(0)
|
|
72
|
+
expect(result.Time).toBeGreaterThan(0)
|
|
73
|
+
expect(result.Time).toBeLessThan(5)
|
|
74
|
+
})
|
|
75
|
+
it('should allow adjacent article and colour spans in specified patterns (lowa)', async function () {
|
|
76
|
+
// When article and colour abut in the cleaned filename (e.g. LA830581 + black-grey),
|
|
77
|
+
// checkPattern must treat touching spans as non-overlapping so the real colour wins
|
|
78
|
+
// over shorter substring colours (black).
|
|
79
|
+
const result = await testBatch('lowa', ['articleCodeSupplier', 'colorCodeSupplier'])
|
|
80
|
+
expect(Object.keys(result).length).toBe(8)
|
|
81
|
+
expect(result.Brand).toBe('lowa')
|
|
82
|
+
expect(result.Mismatches).toBe(0)
|
|
83
|
+
expect(result.QuickSearch).toBe(0)
|
|
84
|
+
expect(result.FallbackSearch).toBe(0)
|
|
85
|
+
expect(result.Time).toBeGreaterThan(0)
|
|
86
|
+
expect(result.Time).toBeLessThan(5)
|
|
87
|
+
}, 140000)
|
|
88
|
+
it('should prefer combinations that still match after dropping a junk last token (exxcellent)', async function () {
|
|
89
|
+
// Equal-length colours can both match the full stem (e.g. 001 vs 014 on …001_4).
|
|
90
|
+
// After removeLastToken, only the true colour remains — keep that combination.
|
|
91
|
+
const result = await testBatch('exxcellent', ['articleCodeSupplier', 'colorCodeSupplier'])
|
|
92
|
+
expect(Object.keys(result).length).toBe(8)
|
|
93
|
+
expect(result.Brand).toBe('exxcellent')
|
|
94
|
+
expect(result.Mismatches).toBe(0)
|
|
95
|
+
expect(result.QuickSearch).toBe(0)
|
|
96
|
+
expect(result.FallbackSearch).toBe(0)
|
|
97
|
+
expect(result.Time).toBeGreaterThan(0)
|
|
98
|
+
expect(result.Time).toBeLessThan(5)
|
|
99
|
+
})
|
|
51
100
|
})
|