mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
77 lines
2.1 KiB
JavaScript
77 lines
2.1 KiB
JavaScript
// 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.5) {
|
|
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);
|
|
};
|
|
}
|