SKUDataFilter 1.0.2

SKUDataFilter 1.0.2

sunrise 维护。



SKUFilterManager

Version License Platform

SKU 商品规格组合算法

DEMO 效果展示

003.gif

目录

关于SKU

问题与思路

算法优化

解决方案-SKUDataFilter

更新日志

博客

关于SKU

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

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

问题与思路

我们所说的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

但是 这种方法有漏洞

选择 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即可(程序员都是从0开始计数的)。

例如,总共有5个条件式,每个条件式中有5个属性,你需要找出满足某个属性的所有条件式。如果你不中断遍历,就需要进行25次判断,而这种方式只需要进行5次判断,所以其优化性非常高。

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

解决方案-SKUDataFilter

SKUDataFilter 正是基于上述分析和算法优化实现的,其关键在于indexPathconditionIndexs。使用NSIndexPath记录每个属性的坐标,使用 conditionIndexs 记录属性 indexPath 中的item。上述已经详细说明了它们的原理和作用,此处再举一个例子来说明SKUDataFilter中的用法。

例:

  颜色: r g
  尺寸: s m l

indexPath 记录属性的位置坐标,表示为 section 种属性类下面的 第item个属性(从0开始计数)

例如,上述示例中,属性m的indexPath表示为 secton : 1, item : 1

conditionIndexs 条件式下标:记录属性 indexPath 中的 item

例如,上述示例中,条件condition (g,l) 的conditionIndexs表示为 (1,2)


判断属性是否存在于条件式中,只需要这样

conditionIndexs[indexPath.section] == indexPath.row

数据通配

conditionIndexsindexPath的结合 对于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;
//当前所有可用结果的结果查询, 一般用于 价格区间 动态变化
@property (nonatomic, strong, readonly) NSSet *currentAvailableResutls;

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

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

6、默认选中第一组SKU

//是否需要默认选中第一组SKU
@property (nonatomic, assign) BOOL needDefaultValue;

使用注意

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

更新日志

2019.07.23
  • 加入属性 当前所有可用结果的结果查询,一般用于价格区间动态变化
@property (nonatomic, strong, readonly) NSSet *currentAvailableResutls;
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
  • 支持cocoapods导入

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

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