ios SKU 组合算法

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ios SKU 组合算法相关的知识,希望对你有一定的参考价值。

参考技术A 通俗来讲,一个SKU 就是商品在规格上的一种组合,比如说,一件衣服 有红色 M号的 也有蓝色 L号的 ,不同的组合就是不同的SKU

近段时间,刚好遇到要在商品详情页购买商品的时候,实现选择不同规格组合的sku,预判无库存sku选项置灰,减少客户不必要sku的选择。

网上搜寻了一大批有关sku选择算法的文章,然后被各路大神的一顿操作秀得一脸懵逼,简单来说就是没看懂。。。

当然基于以上参考得到的灵感,终于总结出来了一种简单易用通俗易懂的sku选择算法,思路简单,唯一的bug是sku数据有n多层的时候,计算量大耗内存。当然现在手机的运算能力都是杠杠的,正常来说商品的sku也不会有几十上百层那么夸张。

接下来我先捋一下思路吧!

Tips: sku 有三种状态,可选(正常),不可选(置灰),已选中(高亮),

一,sku算法初版:计算所有sku的组合 与 有库存sku的组合的交集,交集里面的sku为可选项,反之其他sku为不可选。

 1.计算所有sku的组合-->集合A

A = ["34,61,66"  , "34,61,67"  , ......]

2.计算有库存的sku的组合 -->集合B

一般是从后台服务器返回的 eg:

3. 计算集合A与集合B的交集,交集里面的所有元素就是初始时所有可选sku ID ,反之其他sku ID就是置灰(无库存不可选状态)

4.以上三步就是简易的sku算法核心思路,弹出规格框时,计算集合A和集合B的交集,得到初步赛选结果,告诉客户,哪些sku无库存不可选置灰显示,可选的为正常状态显示,减少客户做不必要的选择操作。

5.当然,细心的你很快就会发现这样的sku算法会导致无法判断出,已选sku的兄弟节点是否可选的bug。

二,优化兄弟节点的可选状态判断bug

1.如上图 已选Platform 属性的 34,长度属性的 62 , 我们要判断的已选sku兄弟节点属性分别是Platform 属性的 35,长度属性的 61。 

2.即:

要判断 长度属性的 61是否为可选,就要判断,34,61这样的组合是否属于有库存组合里面子集,是:可选,不是: 不可选。

同理:

要判断 Platform 属性的 35是否为可选,就要判断,35,62这样的组合是否属于有库存组合里面子集,是:可选,不是: 不可选。

3.细心的你肯定发现了规律,34,61 或者 35,62 这样的组合都有一个共同点

即:包含n个已选skuID,n = 已选sku个数-1 .

三,计算兄弟节点是否可选

1,计算已选sku ID 同类属性的组合 ==集合C  即:计算Platform 属性和长度属性的组合

集合C = ["34,61","34,62","35,61","35,61","35,62"]

2.计算已选skuID的子集 ==集合D 即:计算 [34,62]的子集 

集合D = ["34","62","34,62"]

3.从集合D里面筛选出,含有n个已选skuID的子集(n = 已选sku个数-1 )==集合E

集合E = ["34","62"]

4.最后计算可选兄弟节点组合,集合C里面的组合只要是含有集合E里面的元素都是可选兄弟节点组合

可选兄弟节点组合F = 【集合C里面的组合只要是含有集合E里面的元素】

5. 

兄弟节点可选skuID =  【(集合A与集合B的交集的skuID集合)再与 集合F 的并集 】

四,整理

1.筛选有库存的sku组合为可选sku 其余为不可选sku

2.计算兄弟节点可选sku

3.可选sku正常显示,不可选sku置灰,已选sku高亮

附Demo: sku算法demo

五,拓展一下

iOS SKU规格组合算法

写在前面

本篇文章主要是讲 SKU 商品规格组合的 问题、解决思路及算法优化。 最后 将提供一个SKU算法的通配方案 - SKUDataFilter

本篇文章分析较为详细,针对于对SKU问题不甚了解的童鞋。

不想听我瞎BB的,这边是干货地址

文章最后是 使用说明

更新日志

2019.04.12
  • 加入是否默认选中第一组SKU的控制
    此处是选中第一组SKU, 并不一定包含第一个属性

    // needDefaultValue
    _filter.needDefaultValue = YES;
    [self.collectionView reloadData]; //更新UI显示
    [self action_complete:nil];       //更新结果查询
    
2018.07.11 ~ cocoapods version 1.0.1
  • 支持cocopods 导入

    pod ‘SKUDataFilter‘
    
  • 升级数据防崩溃过滤,即使sku-condition完全对不上号,也不会闪退了。(针对某些极端测试人员)

2018.06.21 -
  • 最近收到很多因为部分sku信息不完整,导致崩溃的反馈。所以新增了sku-condition的检测,过滤并提示了不完整的condition。已更新
2017.12.16 -
  • 由于之前的疏忽,在更新算法的时候,漏了一个点,导致一个非常严重的bug,感谢简书网友@毕小强指出,已更新

关于SKU

维基百科:
最小库存管理单元(Stock Keeping Unit, SKU)是一个会计学名词,定义为库存管理中的最小可用单元。
最小库存管理单元就是“单品” 最小库存单元是指包含特定的自然属性与社会属性的商品种类,在零售连锁门店管理中通常称为“单品”。对于一种商品而言,当他的品牌、型号、配置、花色、容量、生产日期、保质期、用途、价格、产地等属性与其他商品存在不同时,就是一个不同的最小存货单元。

通俗来讲,一个SKU 就是商品在规格上的一种组合,比如说,一件衣服 有红色 M号的 也有蓝色 L号的 ,不同的组合就是不同的SKU

下图DEMO演示效果图

 
技术图片
003.gif

问题与思路

我们所说的SKU 组合算法,就是对商品规格组合的一种筛选和过滤。即 根据已选中的一个或多个属性过滤出 剩余属性的 可选性,以及选完所有属性之后对应的 结果(库存、价格等)

这里的问题就有两个

  • 根据已选中的一个或多个属性过滤出 剩余属性的 可选性
  • 根据选中的所有属性查询对应的结果(库存、价格等)

第二个问题较简单,只需要遍历一遍SKU,找到对应的结果即可,重点在第一个

简单举个例子

**商品规格  :
款式 : F    M
颜色 : R    G    B
尺寸 : L    X    S    

SKU:  
M,G,X  -  66元,10件
F,G,S  -  88元,12件
F,R,X  -  99元,15件  

我们把 一组满足条件的属性 叫做条件式 ,那么这里就有三个条件式
用个图来表示他们之间的关系 (红线为F-G-S)


 
技术图片
condition.png

这里的属性的状态只有两种 可选和不可选。(已选是属于可选)
那么B、L自始至终就为不可选状态

当我们选中某个属性时,比如G -- 那么对应的
可选择属性即为 :
G本身;
兄弟节点(同类可选属性可切换) R(B已淘汰);
对应条件式中的其他节点 M、X、F、S;

乍一看,除了条件式外的都可选,这是因为故意弄成这个条件式以便于后面的讲解。

实际上我们是通过遍历他的兄弟属性, 遍历他所在的条件式,拿出对应的属性。在多个条件式中会有重复的属性,为了过滤重复的值可以利用集合来添加保存的(NSSet,NSMutableSet)

当我们选中多个属性, 比如 F、 R, 由于已选属性之间的相互牵制、这里情况就要复杂的多了

根据上面的分析,我们通常都会想到,遍历各自的兄弟节点,以及条件式,最后各自所取的属性值 去一个交集

(ps1:有的小伙伴可能看不懂,最终可选属性,最傻的办法就是你把可选属性带入条件式里面满足条件即可,兄弟属性在替换之后满足条件式也是可选,比如选中R、他会替换原先的G,也满足已选中属性X,即为可选)

(ps2:排列顺序为:本身、可选兄弟属性、条件式)

F-可选:F、M、R、X、G、S
R-可选:R、G、F、X
交集:F、R、G、X
手动验证,完全OK

But这种方法有漏洞

选择 G、X
G-可选:G、R、F、S、M、X
X-可选:X、S、F、R、M、G
交集:G、X、R、S、R、F、M
手动验证,错误 - F应该为不可选

手动原因:F既在G的条件式F-G-S中,又在X的条件式F-R-X中,但是却不同时满足G、X

这里首先要搞明白 问题绝对不会出现在已选属性的兄弟属性上,因为兄弟节点,在任何一个兄弟属性存在的条件式中 其他兄弟属性都不会出现,有F的条件式就不会有M。所以问题还是在条件式中。当有多个属性被选中时,判断一个非可选属性的兄弟属性是否可选,必须要满足所有可选属性的条件式。那么整体结论即判定某个属性的可选性:该属性要么同时满足所有已选属性的条件式,要么和已选中的某个属性是兄弟属性

算法优化

基于上面的思路,再来说一下算法的优化

  • 降低已选属性的遍历

将上诉理论应用到实际代码中,一般是这样的,再求可选属性集合时:每次一个新的属性操作(选中、取消、切换),都会根据上诉结论 分别为每一个已选属性的筛选出对应的 可选属性,然后在做交集。
这样的话,每次一个新的属性操作,都可能会把的已选属性重复查询一遍。

优化方案构想-每次新的属性操作,只筛选当前属性的可选属性,然后在已选属性的基础上进行增删操作。
看到构想,感觉也不是很难,下面是实际情况——
实际操作分为三种情况:

1、选中新属性 2、切换兄弟属性 3、取消已选中属性

第一种情况,和我们前面的思路吻合,只需要将筛选出新属性对应的可选属性集合,然后与当前的 可选集合 求得 交集即可

后面两种情况,都含有一个取消操作(切换兄弟属性、需要取消上一个兄弟属性),取消操作,意味着,你要把该属性所过滤掉的可选属性,还回来,也就是恢复取交集前的 原集合。那问题的关键即为如何找到这些被该属性过滤掉的可选属性集合或是直接恢复原集合

恢复:可以通过找到所有的原可选集合,并记录下来,然后根据取消属性的匹配原集合恢复(这里不能单纯的记录选中操作的可选集合,因为取消的顺序不一样)
查找过滤掉的可选集合:操作等同于重新计算可选集合

以上两种方案都不可行。这里就不多做赘述,实际操作起来,遍历查询的次数更多了,不仅达不到优化的效果,还增加了算法的复杂程度(这里如果小伙伴有更好的想法,欢迎讨论)。那么最终结论是:在对属性有新操作时,只有新增属性可以基于当前可选属性集合过滤,其他情况需要重新计算

  • 优化条件式

如果说上一个优化方案只是在瞎扯蛋的话,那这里就是整个优化所在的关键了,同时也是SKUDataFilter的核心

认真看了整篇文章就会发现,整个算法思路的核心,在于条件式,不管是查询结果,还是查询可选属性集合,实际上都是依赖于条件式的,我们在查询某个属性时候可选,实际上是要遍历他所在的条件式列表,这个列表又要求我们去遍历所有的条件式,判断这个属性是否在条件式中,拿到列表之后,获取非兄弟属性又要遍历这个属性,是否同时满足所有已选属性的条件式。那么我们整个算法循环次数最多的地方便是判断某个属性是否存在于某个条件式中

**商品规格  :
为规格属性加一个坐标,记录他们的位置

    0    1    2
0   F    M
1   R    G    B
2   L    X    S    

SKU:  用下标表示条件式
M,G,X  -  66元,10件 --- (1,1,1)
F,G,S  -  88元,12件 --- (0,1,2)
F,R,X  -  99元,15件 --- (0,0,1)

在上一个例子中,为每个属性加一个坐标,如L表示为(0, 2)
条件式中用坐标的某一部分表示

这样一来

判断某个属性是否存在于某个条件式:
正常的操作是遍历条件式中的属性,分别和该属性做判断(containsObject方法 本质上也是在做遍历) 。

而这里只需要一次判断就够了 ,设该属性的坐标为(x,y)判断条件式里的第y个值是否等于x即可(这里的判断取决于条件式存入的是x、还是y)

如 判断M(1,0) 是否在F-G-S(0,1,2)条件式中 即条件式的第0个值是否等于1就OK了(程序猿都是从0开始数的)

比如说,总共有5个条件式,每个条件式中有5个属性,你要找出某个满足某个属性的所有条件式
如果你不去中断遍历,就要判断25次,这种方式只需要判定5次就够了,所以它的优化性实际上是非常高的。

实际上,真正神奇之处就在于这样的下标条件式可以清楚的 知道他所拥有的 任何一个属性 的坐标,进而知道属性的值

解决方案-SKUDataFilter

SKUDataFilter 正是基于以上分析和算法优化实现的,使用NSIndexPath记录每个属性的坐标,更加直观。表示为 第section 种属性类下面的 第item个 属性 (从0计数)
条件式下标(conditionIndexs)中记录的属性indexPath的 item

   // 判断属性是否存在于条件式中
   conditionIndexs[indexPath.section] == indexPath.row

数据通配
conditionIndexs 和indexPath的结合 为SKUDataFilter 不仅算法上取得了优势,同时也在 数据通配 上起了莫大的作用

不同的后台,不同的需求,返回的数据结构都不一样。

然而SKUDataFilter真正关心的是属性的坐标,而不是属性本身的的值,那么不管你从后台获取的数据结构是怎样的,也不管你是如何解析的。当然,你也不需要去关心坐标和条件式下标等等乱七八糟的。你需要做的只是把对应的数据放入对应的代理方法里面去就行了,不管数据是model,属性ID、字典还是其他的。

使用说明

使用可以参考SKUDataFilterDemo

SKUDataFilter最终直接反映的是属性的indexPath, 如果你的属性在UI显示上使用UICollectionView实现,那么indexPath是一一对应的,如果用的循环创建,找到对应的行和列即可。

1、初始化Filter 并设置代理

 - (instancetype)initWithDataSource:(id<ORSKUDataFilterDataSource>)dataSource;

  //当数据更新的时候 重新加载数据
 - (void)reloadData;

2、通过代理方法 ,将数据传给Filter

以下方法都必需实现,分别告诉Filter,属性种类个数、每个种类的所有属性(数组),条件式个数、每个条件式包含的所有属性、以及每个条件式对应的结果(可以参考本文案例)

  //属性种类个数
  - (NSInteger)numberOfSectionsForPropertiesInFilter:(ORSKUDataFilter *)filter;

  /*
   * 每个种类所有的的属性值
   * 这里不关心具体的值,可以是属性ID, 属性名,字典、model
   */
  - (NSArray *)filter:(ORSKUDataFilter *)filter propertiesInSection:(NSInteger)section;

  //满足条件 的 个数
  - (NSInteger)numberOfConditionsInFilter:(ORSKUDataFilter *)filter;

  /*
   * 对应的条件式
   * 这里条件式的属性值,需要和filter:propertiesInSection里面的数据 类型保持一致
   */
  - (NSArray *)filter:(ORSKUDataFilter *)filter conditionForRow:(NSInteger)row;

  //条件式 对应的 结果数据(库存、价格等)
  - (id)filter:(ORSKUDataFilter *)filter resultOfConditionForRow:(NSInteger)row;

3、点击某个属性的时候 把对应属性的indexPath传给Filter

- (void)didSelectedPropertyWithIndexPath:(NSIndexPath *)indexPath;

4、查询结果(与代理方法resultOfConditionForRow:对应)-条件不完整会返回nil

 @property (nonatomic, strong, readonly) id  currentResult;

5、可选属性集合列表、已选属性坐标列表

//当前 选中的属性indexPath
@property (nonatomic, strong, readonly) NSArray <NSIndexPath *> *selectedIndexPaths;
//当前 可选的属性indexPath
@property (nonatomic, strong, readonly) NSArray <NSIndexPath *> *availableIndexPaths;

使用注意
1、虽然SKUDataFilter不关心具体的值,但是条件式本质是由属性组成,故代理方法filter:propertiesInSection:和方法filter:conditionForRow:数据类型应该保持一致
2、因为SKUDataFilter关心的是属性的坐标,那么在代理方法传值的时候,代理方法filter:propertiesInSection:和方法filter:conditionForRow:各自的数据顺序要保持一致 并且两个方法的数据也要对应
如本文案例条件式是从上往下(M,G,X),传过去的 属性值 也都是从左到右(F、M)-各自保持一致。 同时
条件式为从上到下,那么propertiesInSection: 也应该是从上到下,先是(F、M)最后是(L、X、S)
实际项目中,这两种情况发生的概率都非常小,因为 第一数据统一返回统一解析,格式99%都是一样。第二数据是从服务器返回,服务器的数据要进行筛选和过滤,顺序也不能弄错,一旦错误,首先服务器就会出问题

 

作者:OrangeAL
链接:https://www.jianshu.com/p/295737e2ac77
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

以上是关于ios SKU 组合算法的主要内容,如果未能解决你的问题,请参考以下文章

淘宝sku算法浅析

sku多规格属性组合库存判断 - iOS

js实现简单sku变体组合算法

js实现简单sku变体组合算法

iOS小技能:SKU视图搭建

iOS小技能:SKU视图搭建