前端sku算法

416 阅读3分钟

笛卡尔积算法

规格属性目标结果
[['红色', '白色']][['红色'],['白色']]
[['红色','白色'],['S', 'M']][['红色','S'],['红色','M'],['白色','S'],['白色','M']]

SKU 组合实现思路

首先让我们来看看笛卡尔积的描述

笛卡尔乘积是指在数学中,两个[集合] X 和 Y 的笛卡尔积(Cartesian product),又称 [ 直积 ] ,表示为 X × Y,第一个对象是 X 的成员而第二个对象是 Y 的所有可能 [ 有序对 ] 的其中一个成员 假设集合 A = { a, b },集合 B = { 0, 1, 2 },则两个集合的笛卡尔积为 { ( a, 0 ), ( a, 1 ), ( a, 2), ( b, 0), ( b, 1), ( b, 2) }

笛卡尔积 通过上面的思维导图,可以看出这种规格组合是一个经典的排列组合,去组合每一个规格值得到最终 SKU。

那么让我们来进行代码实现,看看代码如何实现笛卡尔积。

const isNil = (obj) => obj === undefined || obj === null;

function wrapInArray(v) {
  if (isNil(v)) return [];
  return Array.isArray(v) ? v : [v];
}

function calcDescartes(array) {
  if (!array?.length) return [];
  if (array.length === 1) return array[0].map((item) => [item]);

  return array.reduce((total, currentValue) => {
    let res = [];

    total.forEach((t) => {
      currentValue.forEach((cv) => {
        res.push([...wrapInArray(t), cv]);
      });
    });
    return res;
  });
}

生成规格列表

业务场景

原始的数据结构(后端给过来的)

interface BackEndVariationOption {
  name: string;
  options: string[];
}

具体实现

首先给每个规格属性红色, 白色生成一个对应的 uid(避免规格重复的时候) 生成的数据结构

type Uid = string;
interface VariationOptionData {
  name: string;
  options: { uid: Uid; value: string }[];
  uid: Uid;
}

// 规格项
interface VariationIndex {
  uids: Uid[]; // 当前规格项的规格组合`红色, M`
}

生成一个笛卡尔积 uid 数组, 它包含所有的规格列表的组合

const mapSkip = Symbol("skip");

function map(iterable, mapper) {
  const result = [];
  for (let i = 0; i < iterable.length; i++) {
    const item = iterable[i];
    const element = mapper(item, i, iterable);
    if (element === mapSkip) continue;
    result.push(element);
  }
  return result;
}

function getDescartesUidArray(variations: VariationOptionData[]): Uid[][] {
  const optionsArray = map(variations, ({ options }) =>
    options?.length ? options.map(({ uid }) => uid) : mapSkip
  );
  return calcDescartes(optionsArray);
}

规格属性的编辑只有两种情况

  • 增加
    • 总体思想: 通过遍历笛卡尔积 uid 数组, 更新相似的改变, 填充缺失的规格项
  • 删除(更新)
    • 总体思想: 通过遍历规格项列表, 更新相似的改变, 并且清除多余的。

那么什么是相似的改变呢?

假设有[['红色','白色'],['S']]规格, 转换成前端的结构就是

const varitions = [
  {
    name: "颜色",
    options: [
      { value: "红色", uid: 0 },
      { value: "白色", uid: 1 },
    ],
  },
  { name: "尺寸", options: [{ value: "S", uid: 2 }] },
];

通过上述笛卡尔积生成uid数组就是[[0,2],[1,2]] 现增加一列年龄的规格{name: '年龄', options:[{value: '12岁', uid: 3}]} 笛卡尔积uid数组就变成了[[0,2,3],[1,2,3]] [0,2]=>[0,2,3], 那么逻辑上只要判断旧的uid数组被包含于新的uid就好了 删除规格的话就正好相反, [0,2,3]=> [0,2],

/**
 * @description 两数组是否相似,b可以存在a中不存在的值
 * b->[_,...a]
 * */
function isSimilarArray(a, b): boolean {
  return !a.some((item) => !b.includes(item))
}

type UpdateItemFn<T> = (
  item: T,
  uids: Uid[],
  variations: VariationOptionData[]
) => void

function addVariationOption<T: BaseVariationIndex>(
  items: T[],
  {
    variations,
    descartesUidArray,
    createItem = (uids, variations) =>
      new BaseVariationIndex({
        uids,
        index: uidsToIndex(uids, variations),
      }),
    updateItem = (item, uids, variations) => {
      item.index = uidsToIndex(uids, variations)
    },
    addItems = [],
  }: {
    variations: VariationOptionData[],
    descartesUidArray?: Uid[][],
    createItem?: (uids: Uid[], variations: VariationOptionData[]) => T,
    updateItem?: UpdateItemFn<T>,
    force?: boolean,
    addItems?: VariationOptionValue[],
  }
): void {
  descartesUidArray = descartesUidArray ?? getDescartesUidArray(variations)
  descartesUidArray.forEach((uids) => {
    const findIndex = items.findIndex((variationTableItem) => {
      const res = isSimilarArray(variationTableItem.uids, uids)
      if (res) {
        variationTableItem.uids = uids
        updateItem?.(variationTableItem, uids, variations)
      }
      return res
    })
    if (findIndex !== -1) return
    const isContain = addItems?.length
      ? uids.some((uid) => addItems.some((item) => uid === item.uid))
      : true
    if (!isContain) return
    const addItem = createItem(uids, variations)
    items.push(addItem)
  })
}

function removeVariationOption<T: BaseVariationIndex>(
  items: T[],
  {
    variations,
    descartesUidArray,
    updateItem = (item, uids, variations) => {
      item.index = uidsToIndex(uids, variations)
    },
  }: {
    variations: VariationOptionData[],
    descartesUidArray?: Uid[][],
    needSort?: boolean,
    updateItem?: UpdateItemFn<T>,
  }
): void {
  const removeIndex = []
  const memo = []
  descartesUidArray = descartesUidArray ?? getDescartesUidArray(variations)
  items.forEach((variationTableItem, index) => {
    let res = true
    descartesUidArray.forEach((uids, index) => {
      const result = isSimilarArray(uids, variationTableItem.uids)
      if (result) {
        if (!memo[index]) res = false
        variationTableItem.uids = uids
        updateItem?.(variationTableItem, uids, variations)
        memo[index] = true
      }
    })
    if (res) removeIndex.push(index)
  })
  const len = removeIndex.length
  for (let i = len - 1; i >= 0; i--) {
    const item = removeIndex[i]
    items.splice(item, 1)
  }
}

vue3 的 hooks 写法

function useVariationOption<T: BaseVariationIndex>(
  variations: Ref<VariationOptionData[]>,
  variationIndexs: Ref<BaseVariationIndex[]>,
  createItem?: (uids: Uid[], variations: VariationOptionData[]) => T,
  updateItem?: UpdateItemFn<T>
) {
  const descartesUidArray = computed(() =>
    getDescartesUidArray(variations.value)
  )
  return {
    descartesUidArray,
    add: (addItems) => {
      addVariationOption(variationIndexs.value, {
        variations: variations.value,
        descartesUidArray: descartesUidArray.value,
        updateItem,
        createItem,
        addItems: wrapInArray(addItems),
      })
    },
    remove: () => {
      removeVariationOption<T>(variationIndexs.value, {
        variations: variations.value,
        descartesUidArray: descartesUidArray.value,
        updateItem,
      })
    },
  }
}