From 44e8b5718f44025d2f9b224650b13c0b2c19907a Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:11:31 +0700 Subject: [PATCH 1/3] fix(importer): watch band size range and list before the band key size-less MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A band size range written with " - " or an en dash between two bare sizes ("Sport Loop 42mm - 46mm"), and two or more sizes listed right before a band word ("GPS 40mm, 44mm Dây Sport Band"), name the band's sizes, not the case. Both now key size-less. A dashed size with more text after it, a single size before the band and a part that names the case keep their key. Closes #1162 Co-Authored-By: Claude Opus 5.5 --- .../__tests__/open-core-intelligence.test.ts | 25 +++++++++++++ .../importer/src/shopee/product-normalizer.ts | 37 +++++++++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) 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..841abcd6 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,31 @@ 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'], + ]; + 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..250d0880 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,45 @@ 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(''); + 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"). + const sizeBeforeBand = band.length > 0 && LONE_WATCH_SIZE.test(before!); + 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 headListed = + sizeBeforeBand && + k > 0 && + listSepAt(k) && + LAST_WATCH_SIZE.test(outside[k - 1]!) && + !WATCH_CASE_WORD.test(parts[2 * k - 2]!.normalize('NFC')); + if (k < outside.length || 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`; From 004007be405da88ae1a79a39688fdf4e7542e78d Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:15:28 +0700 Subject: [PATCH 2/3] fix(importer): a band-led size or a case word before the list keeps the watch case size Co-Authored-By: Claude Opus 5.5 --- .../shopee/__tests__/open-core-intelligence.test.ts | 9 +++++++++ apps/importer/src/shopee/product-normalizer.ts | 13 +++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) 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 841abcd6..16f02b6a 100644 --- a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts +++ b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts @@ -524,6 +524,15 @@ async function runSuite() { ], ['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', + ], + // 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); diff --git a/apps/importer/src/shopee/product-normalizer.ts b/apps/importer/src/shopee/product-normalizer.ts index 250d0880..31b76ebc 100644 --- a/apps/importer/src/shopee/product-normalizer.ts +++ b/apps/importer/src/shopee/product-normalizer.ts @@ -1217,19 +1217,24 @@ function readAppleSize(model: string, title: string): string { } const [before, ...band] = part.split(WATCH_BAND_WORD); // 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"). + // 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. const sizeBeforeBand = band.length > 0 && LONE_WATCH_SIZE.test(before!); 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(outside[k - 1]!) && - !WATCH_CASE_WORD.test(parts[2 * k - 2]!.normalize('NFC')); - if (k < outside.length || headListed) { + 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(''); From 99800ec6df92a34270dfe8d6255eabdd385b8295 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:20:10 +0700 Subject: [PATCH 3/3] fix(importer): watch range follows the last listed size; a band naming the case keeps it Co-Authored-By: Claude Opus 5.5 --- .../shopee/__tests__/open-core-intelligence.test.ts | 10 ++++++++++ apps/importer/src/shopee/product-normalizer.ts | 10 ++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) 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 16f02b6a..93980e9b 100644 --- a/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts +++ b/apps/importer/src/shopee/__tests__/open-core-intelligence.test.ts @@ -528,6 +528,16 @@ async function runSuite() { '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', diff --git a/apps/importer/src/shopee/product-normalizer.ts b/apps/importer/src/shopee/product-normalizer.ts index 31b76ebc..531136a4 100644 --- a/apps/importer/src/shopee/product-normalizer.ts +++ b/apps/importer/src/shopee/product-normalizer.ts @@ -1213,14 +1213,20 @@ function readAppleSize(model: string, title: string): string { 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); // 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. - const sizeBeforeBand = band.length > 0 && LONE_WATCH_SIZE.test(before!); + // 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;