@softwear/latestcollectioncore 1.0.202 → 1.0.204
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/articleStatus.js +7 -17
- package/dist/imageBinder.d.ts +1 -1
- package/dist/imageBinder.js +253 -77
- package/package.json +1 -1
- package/src/articleStatus.ts +7 -41
- package/src/imageBinder.ts +245 -79
- 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/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
|
})
|