spirit-tracker/viz/app/linker/price.js
Brennan Wilkes (Text Groove) 7a33d51c90 UX Improvements
2026-02-10 16:45:22 -08:00

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);
};
}