mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
link sku
This commit is contained in:
parent
91f6288b43
commit
5f7eeb5205
3 changed files with 106 additions and 3 deletions
78
viz/app/linker/price.js
Normal file
78
viz/app/linker/price.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// viz/app/linker/price.js
|
||||
export function buildPricePenaltyForPair({ allAgg, rules, kPerGroup = 6 }) {
|
||||
// canonSku -> sorted array of up to kPerGroup lowest prices
|
||||
const groupPrices = new Map();
|
||||
|
||||
function insertPrice(arr, p) {
|
||||
// keep sorted ascending, cap length
|
||||
let i = 0;
|
||||
while (i < arr.length && arr[i] <= p) i++;
|
||||
arr.splice(i, 0, p);
|
||||
if (arr.length > kPerGroup) arr.length = kPerGroup;
|
||||
}
|
||||
|
||||
for (const it of allAgg || []) {
|
||||
if (!it) continue;
|
||||
const sku = String(it.sku || "");
|
||||
if (!sku) continue;
|
||||
|
||||
const p = it.cheapestPriceNum;
|
||||
if (p == null || !(p > 0)) continue;
|
||||
|
||||
const canon = String((rules && rules.canonicalSku && rules.canonicalSku(sku)) || sku);
|
||||
let arr = groupPrices.get(canon);
|
||||
if (!arr) groupPrices.set(canon, (arr = []));
|
||||
insertPrice(arr, p);
|
||||
}
|
||||
|
||||
function bestRelativeGap(prA, prB) {
|
||||
// min |a-b| / min(a,b)
|
||||
let best = Infinity;
|
||||
for (let i = 0; i < prA.length; i++) {
|
||||
const a = prA[i];
|
||||
for (let j = 0; j < prB.length; j++) {
|
||||
const b = prB[j];
|
||||
const gap = Math.abs(a - b) / Math.max(1e-9, Math.min(a, b));
|
||||
if (gap < best) best = gap;
|
||||
if (best <= 0.001) return best;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function gapToMultiplier(gap) {
|
||||
// gap = 0.40 => 40% relative difference
|
||||
// <=35%: no penalty
|
||||
// 35-50%: ease down to ~0.75
|
||||
// >50%: continue down gently, floor at 0.35
|
||||
if (!(gap >= 0)) return 1.0;
|
||||
if (gap <= 0.35) return 1.0;
|
||||
|
||||
if (gap <= 0.50) {
|
||||
const t = (gap - 0.35) / 0.15; // 0..1
|
||||
return 1.0 - 0.25 * t; // 1.00 -> 0.75
|
||||
}
|
||||
|
||||
const m = 0.75 * (0.5 / gap);
|
||||
return Math.max(0.35, m);
|
||||
}
|
||||
|
||||
return function pricePenaltyForPair(aSku, bSku) {
|
||||
const a = String(aSku || "");
|
||||
const b = String(bSku || "");
|
||||
if (!a || !b) return 1.0;
|
||||
|
||||
const aCanon = String((rules && rules.canonicalSku && rules.canonicalSku(a)) || a);
|
||||
const bCanon = String((rules && rules.canonicalSku && rules.canonicalSku(b)) || b);
|
||||
|
||||
const prA = groupPrices.get(aCanon);
|
||||
const prB = groupPrices.get(bCanon);
|
||||
if (!prA || !prB || !prA.length || !prB.length) return 1.0;
|
||||
|
||||
const gap = bestRelativeGap(prA, prB);
|
||||
if (!isFinite(gap)) return 1.0;
|
||||
|
||||
return gapToMultiplier(gap);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +63,7 @@ export function recommendSimilar(
|
|||
mappedSkus,
|
||||
isIgnoredPairFn,
|
||||
sizePenaltyFn,
|
||||
pricePenaltyFn,
|
||||
sameStoreFn,
|
||||
sameGroupFn
|
||||
) {
|
||||
|
|
@ -170,6 +171,11 @@ export function recommendSimilar(
|
|||
s0 *= sizePenaltyFn(pinnedSku, itSku);
|
||||
}
|
||||
|
||||
// Price penalty early
|
||||
if (typeof pricePenaltyFn === "function") {
|
||||
s0 *= pricePenaltyFn(pinnedSku, itSku);
|
||||
}
|
||||
|
||||
// Age handling early
|
||||
const itAge = extractAgeFromText(itNorm);
|
||||
if (pinAge && itAge) {
|
||||
|
|
@ -216,6 +222,11 @@ export function recommendSimilar(
|
|||
if (s <= 0) continue;
|
||||
}
|
||||
|
||||
if (typeof pricePenaltyFn === "function") {
|
||||
s *= pricePenaltyFn(pinnedSku, itSku);
|
||||
if (s <= 0) continue;
|
||||
}
|
||||
|
||||
const itAge = extractAgeFromText(itNorm);
|
||||
if (pinAge && itAge) {
|
||||
if (pinAge === itAge) s *= 2.0;
|
||||
|
|
@ -263,7 +274,8 @@ export function recommendSimilar(
|
|||
limitPairs,
|
||||
isIgnoredPairFn,
|
||||
sameStoreFn,
|
||||
sizePenaltyFn // ✅ NEW: pass sizePenaltyForPair in
|
||||
sizePenaltyFn, // ✅ NEW: pass sizePenaltyForPair in
|
||||
pricePenaltyFn
|
||||
) {
|
||||
const itemsAll = allAgg.filter((it) => !!it);
|
||||
|
||||
|
|
@ -456,6 +468,8 @@ export function recommendSimilar(
|
|||
|
||||
if (typeof sizePenaltyFn === "function") s *= sizePenaltyFn(aSku, bSku);
|
||||
|
||||
if (typeof pricePenaltyFn === "function") s *= pricePenaltyFn(aSku, bSku);
|
||||
|
||||
const bAge = extractAgeFromText(bNorm);
|
||||
if (aAge && bAge) {
|
||||
if (aAge === bAge) s *= 1.6;
|
||||
|
|
@ -495,6 +509,11 @@ export function recommendSimilar(
|
|||
if (s <= 0) continue;
|
||||
}
|
||||
|
||||
if (typeof pricePenaltyFn === "function") {
|
||||
s *= pricePenaltyFn(aSku, bSku);
|
||||
if (s <= 0) continue;
|
||||
}
|
||||
|
||||
if (aAge && x.bAge) {
|
||||
if (aAge === x.bAge) s *= 2.0;
|
||||
else s *= 0.15;
|
||||
|
|
@ -577,4 +596,3 @@ export function recommendSimilar(
|
|||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { buildCanonStoreCache, makeSameStoreCanonFn } from "./linker/store_cache
|
|||
import { buildSizePenaltyForPair } from "./linker/size.js";
|
||||
import { pickPreferredCanonical } from "./linker/canonical_pref.js";
|
||||
import { smwsKeyFromName } from "./linker/similarity.js";
|
||||
import { buildPricePenaltyForPair } from "./linker/price.js";
|
||||
import {
|
||||
topSuggestions,
|
||||
recommendSimilar,
|
||||
|
|
@ -124,10 +125,14 @@ export async function renderSkuLinker($app) {
|
|||
// ✅ canonical-group size cache + helper
|
||||
let sizePenaltyForPair = buildSizePenaltyForPair({ allRows, allAgg, rules });
|
||||
|
||||
// ✅ canonical-group price cache + helper
|
||||
let pricePenaltyForPair = buildPricePenaltyForPair({ allAgg, rules });
|
||||
|
||||
function rebuildCachesAfterRulesReload() {
|
||||
CANON_STORE_CACHE = buildCanonStoreCache(allAgg, rules);
|
||||
sameStoreCanon = makeSameStoreCanonFn(rules, CANON_STORE_CACHE);
|
||||
sizePenaltyForPair = buildSizePenaltyForPair({ allRows, allAgg, rules });
|
||||
pricePenaltyForPair = buildPricePenaltyForPair({ allAgg, rules });
|
||||
}
|
||||
|
||||
function isIgnoredPair(a, b) {
|
||||
|
|
@ -156,7 +161,8 @@ export async function renderSkuLinker($app) {
|
|||
28,
|
||||
isIgnoredPair,
|
||||
sameStoreCanon,
|
||||
sizePenaltyForPair // ✅ NEW
|
||||
sizePenaltyForPair, // ✅ NEW
|
||||
pricePenaltyForPair // ✅ NEW
|
||||
);
|
||||
|
||||
return initialPairs;
|
||||
|
|
@ -240,6 +246,7 @@ export async function renderSkuLinker($app) {
|
|||
mappedSkus,
|
||||
isIgnoredPair,
|
||||
sizePenaltyForPair,
|
||||
pricePenaltyForPair,
|
||||
sameStoreCanon,
|
||||
sameGroup
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue