diff --git a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts index 0b66cf00..93980e9b 100644 --- a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts +++ b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts @@ -503,6 +503,50 @@ async function runSuite() { for (const [title, expectedKey] of watchBandListKeys1137) { assert.equal(keyOf(title), expectedKey, title); } + // DOS/Rate#1162: a range of bare band sizes, or two or more sizes listed right before a + // band word, names no case. + const watchBandRangeKeys1162: Array<[string, string]> = [ + // The titles of the issue. + ['Apple Watch Series 10 Sport Loop 42mm - 46mm', 'apple:watch-series-10:unknown'], + ['Apple Watch SE 3 GPS 40mm, 44mm Dây Sport Band', 'apple:watch-se-3:unknown'], + // An en dash range keys like " - "; a pipe or longer list keys like a comma list. + ['Apple Watch Series 10 Sport Loop 42mm \u2013 46mm', 'apple:watch-series-10:unknown'], + ['Apple Watch Series 10 GPS, 42mm, 46mm Dây Sport Loop', 'apple:watch-series-10:unknown'], + ['Apple Watch Series 10 GPS 42mm | 46mm Dây Cao Su', 'apple:watch-series-10:unknown'], + ['Apple Watch Series 10 GPS 46mm - 42mm Dây Sport Band', 'apple:watch-series-10:unknown'], + // A size after the dash with more text, a single size before the band, a size the + // case names or a size cut off by ")" is still the case's. + ['Apple Watch Series 10 Sport Loop 42mm - 46mm GPS', 'apple:watch-series-10-46mm:unknown'], + ['Apple Watch Series 10 GPS, 44mm Dây Sport Band', 'apple:watch-series-10-44mm:unknown'], + [ + 'Apple Watch Series 10 Vỏ nhôm 46mm, 42mm Dây Sport Band', + 'apple:watch-series-10-46mm:unknown', + ], + ['Apple Watch Series 10 (GPS 46mm) 42mm Dây Sport', 'apple:watch-series-10-46mm:unknown'], + ['Apple Watch Series 10 GPS 46mm, 46mm Dây Cao Su', 'apple:watch-series-10-46mm:unknown'], + [ + 'Apple Watch Series 10 (Vỏ nhôm) 46mm, 42mm Dây Sport Band', + 'apple:watch-series-10-46mm:unknown', + ], + [ + 'Apple Watch Series 10 GPS 46mm, 42mm Dây Sport Band mặt 46mm', + 'apple:watch-series-10-46mm:unknown', + ], + // A range runs on from a listed bare size, not from one with a fit after it. + ['Apple Watch Series 10 Sport Band 42mm S/M, 44mm - 46mm', 'apple:watch-series-10:unknown'], + [ + 'Apple Watch Series 10 Sport Band 42mm, 44mm M/L - 46mm', + 'apple:watch-series-10-46mm:unknown', + ], + // A size with a band of its own is no list item: the size before it is the case's. + [ + 'Apple Watch Series 10 GPS 46mm Dây Sport Band, 42mm Dây Cao Su', + 'apple:watch-series-10-46mm:unknown', + ], + ]; + for (const [title, expectedKey] of watchBandRangeKeys1162) { + assert.equal(keyOf(title), expectedKey, title); + } // The registry has one entry per line: a sized title keeps the line's benchmark, but // the entry's screen and battery describe only the size it was written for. assert.equal( diff --git a/apps/importer/src/shopee/product-normalizer.ts b/apps/importer/src/shopee/product-normalizer.ts index a0eb635b..531136a4 100644 --- a/apps/importer/src/shopee/product-normalizer.ts +++ b/apps/importer/src/shopee/product-normalizer.ts @@ -1157,6 +1157,9 @@ const WATCH_BAND_WORD = /(? = { ipad: sizePatterns(IPAD_SIZE), @@ -1194,17 +1197,56 @@ function readAppleSize(model: string, title: string): string { // A band phrase with a size runs on over the parts after it that start with a size // and follow "," or "|": "Sport Loop 42mm | 46mm" or "Dây 42mm S/M, 46mm M/L" names // two band sizes, not a case (DOS/Rate#1137). A part that names the case ("46mm, 42mm - // Aluminum Case") ends the list. + // Aluminum Case") ends the list. An en dash or " - " between a band size and a bare + // size is a range of band sizes ("Sport Loop 42mm - 46mm"), and two or more sizes + // listed right before a band word are the band's ("GPS 40mm, 44mm Dây Sport Band"); a + // size after the dash with more text ("Dây Vải 45mm - 46mm GPS") or a single size + // before the band ("GPS 46mm, Dây Cao Su 46mm S/M") is still the case's (DOS/Rate#1162). const parts = own.split(/([),|;\u2013]| - )/); const outside: string[] = []; let bandList = false; + let bandEnd = false; for (let i = 0; i < parts.length; i += 2) { const part = parts[i]!; + const sep = parts[i - 1] ?? ''; const listed = LIST_WATCH_SIZE.test(part) && !WATCH_CASE_WORD.test(part.normalize('NFC')); - if (bandList && /^[,|]$/.test(parts[i - 1]!) && listed) continue; + const ranged = bandEnd && WATCH_RANGE_SEP.test(sep) && LONE_WATCH_SIZE.test(part); + if (bandList && ((/^[,|]$/.test(sep) && listed) || ranged)) { + outside.push(''); + // "42mm S/M, 44mm - 46mm" ranges on from the listed size; "44mm M/L - 46mm" does not. + bandEnd = LAST_WATCH_SIZE.test(part); + continue; + } const [before, ...band] = part.split(WATCH_BAND_WORD); - outside.push(before!); + // Walk back over the bare sizes listed before this band ("40mm, 44mm Dây"): k ends + // at the first of them, and the part before it may end in one more ("GPS 40mm") if + // it has no band of its own. A part before the list that names the case ("(Vỏ nhôm) + // 46mm, 42mm Dây") keeps the list's first size as the case's, and a band that names + // the case ("42mm Dây Sport Band mặt 46mm") starts no walk. + const sizeBeforeBand = + band.length > 0 && + LONE_WATCH_SIZE.test(before!) && + !WATCH_CASE_WORD.test(part.normalize('NFC')); + const listSepAt = (n: number) => + /^[,|]$/.test(parts[2 * n - 1]!) || WATCH_RANGE_SEP.test(parts[2 * n - 1]!); + let k = outside.length; + while (sizeBeforeBand && k > 0 && listSepAt(k) && LONE_WATCH_SIZE.test(outside[k - 1]!)) k--; + const head = k > 0 ? parts[2 * k - 2]! : ''; + const headCase = WATCH_CASE_WORD.test(head.normalize('NFC')); + const headListed = + sizeBeforeBand && + k > 0 && + listSepAt(k) && + LAST_WATCH_SIZE.test(head) && + !WATCH_BAND_WORD.test(head) && + !headCase; + if ((k < outside.length && !headCase) || headListed) { + for (let j = k; j < outside.length; j++) outside[j] = ''; + if (headListed) outside[k - 1] = outside[k - 1]!.replace(LAST_WATCH_SIZE, ''); + outside.push(''); + } else outside.push(before!); bandList = WATCH_SIZE.test(band.join(' ')); + bandEnd = LAST_WATCH_SIZE.test(band.join(' ')); } const caseSize = outside.join(' ').match(WATCH_SIZE)?.[1]; if (caseSize) return `${caseSize}mm`;