姬長信(Redy)

iOS-SKU商品规格组合算法详解

# 写在前面 --- 本篇文章主要是讲 SKU 商品规格组合的 问题、解决思路及算法优化。 最后 将提供一个SKU算法的通配方案 - **SKUDataFilter** 本篇文章分析较为详细/uff0c针对于对SKU问题不甚了解的童鞋。 不想听我瞎BB的/uff0c这边是[**干货地址**](https://github.com/SunriseOYR/SKUDataFilter) 文章最后是 **使用说明** DEMP效果图 ![003.gif](https://user-gold-cdn.xitu.io/2019/7/22/16c1842452556ea6?w=260&h=525&f=gif&s=50171) # 关于SKU --- >维基百科: 最小库存管理单元/uff08Stock Keeping Unit, SKU/uff09是一个会计学名词/uff0c定义为库存管理中的最小可用单元。 最小库存管理单元就是/u201c单品/u201d 最小库存单元是指包含特定的自然属性与社会属性的商品种类/uff0c在零售连锁门店管理中通常称为/u201c单品/u201d。对于一种商品而言/uff0c当他的品牌、型号、配置、花色、容量、生产日期、保质期、用途、价格、产地等属性与其他商品存在不同时/uff0c就是一个不同的最小存货单元。 通俗来讲/uff0c一个SKU 就是商品在规格上的一种组合/uff0c比如说/uff0c一件衣服 有红色 M号的 也有蓝色 L号的 /uff0c不同的组合就是不同的SKU # 问题与思路 --- 我们所说的SKU 组合算法/uff0c就是对商品规格组合的一种筛选和过滤。即 根据已选中的一个或多个属性过滤出 剩余属性的 可选性/uff0c以及选完所有属性之后对应的 结果/uff08库存、价格等/uff09 这里的问题就有两个 > * 根据已选中的一个或多个属性过滤出 剩余属性的 可选性 >* 根据选中的所有属性查询对应的结果/uff08库存、价格等/uff09 第二个问题较简单/uff0c只需要遍历一遍SKU/uff0c找到对应的结果即可/uff0c重点在第一个 简单举个例子 **商品规格 /uff1a 款式 /uff1a F M 颜色 /uff1a R G B 尺寸 /uff1a L X S SKU: M,G,X - 66元/uff0c10件 F,G,S - 88元/uff0c12件 F,R,X - 99元/uff0c15件 我们把 一组满足条件的属性 叫做条件式 /uff0c那么这里就有三个条件式 用个图来表示他们之间的关系 /uff08红线为F-G-S/uff09 ![condition.png](https://user-gold-cdn.xitu.io/2019/7/22/16c184245268e427?w=230&h=227&f=png&s=22297) 这里的属性的状态只有两种 可选和不可选。/uff08已选是属于可选) 那么B、L自始至终就为不可选状态 **当我们选中某个属性时**/uff0c比如G -- 那么对应的 可选择属性即为 /uff1a G本身/uff1b 兄弟节点/uff08同类可选属性可切换/uff09 R/uff08B已淘汰/uff09/uff1b 对应条件式中的其他节点 M、X、F、S/uff1b 乍一看/uff0c除了条件式外的都可选/uff0c这是因为故意弄成这个条件式以便于后面的讲解。 实际上我们是通过遍历他的兄弟属性/uff0c 遍历他所在的条件式/uff0c拿出对应的属性。在多个条件式中会有重复的属性/uff0c为了过滤重复的值可以利用集合来添加保存的/uff08NSSet,NSMutableSet/uff09 **当我们选中多个属性**/uff0c 比如 F、 R/uff0c 由于已选属性之间的相互牵制、这里情况就要复杂的多了 根据上面的分析/uff0c我们通常都会想到/uff0c遍历各自的兄弟节点/uff0c以及条件式/uff0c最后各自所取的属性值 去一个交集 /uff08ps1/uff1a有的小伙伴可能看不懂/uff0c最终可选属性/uff0c最傻的办法就是你把可选属性带入条件式里面满足条件即可/uff0c兄弟属性在替换之后满足条件式也是可选/uff0c比如选中R、他会替换原先的G,也满足已选中属性X,即为可选/uff09 /uff08ps2/uff1a排列顺序为/uff1a本身、可选兄弟属性、条件式/uff09 F-可选/uff1aF、M、R、X、G、S R-可选/uff1aR、G、F、X 交集/uff1aF、R、G、X 手动验证/uff0c完全OK **But** 这种方法有漏洞 选择 G、X G-可选/uff1aG、R、F、S、M、X X-可选/uff1aX、S、F、R、M、G 交集/uff1aG、X、R、S、R、F、M 手动验证/uff0c错误 - F应该为不可选 >手动原因/uff1aF既在G的条件式F-G-S中/uff0c又在X的条件式F-R-X中/uff0c但是却不同时满足G、X >这里首先要搞明白 问题绝对不会出现在已选属性的兄弟属性上,因为兄弟节点/uff0c在任何一个兄弟属性存在的条件式中 其他兄弟属性都不会出现/uff0c有F的条件式就不会有M。所以问题还是在条件式中。当有多个属性被选中时/uff0c判断一个非可选属性的兄弟属性是否可选/uff0c必须要满足所有可选属性的条件式。那么整体结论即**判定某个属性的可选性/uff1a该属性要么同时满足所有已选属性的条件式/uff0c要么和已选中的某个属性是兄弟属性** # 算法优化 --- 基于上面的思路/uff0c再来说一下算法的优化 * ## 降低已选属性的遍历 将上诉理论应用到实际代码中/uff0c一般是这样的/uff0c再求可选属性集合时/uff1a每次一个新的属性操作/uff08选中、取消、切换)/uff0c都会根据上诉结论 分别为每一个已选属性的筛选出对应的 可选属性/uff0c然后在做交集。 这样的话/uff0c每次一个新的属性操作/uff0c都可能会把的已选属性重复查询一遍。 优化方案构想-**每次新的属性操作/uff0c只筛选当前属性的可选属性/uff0c然后在已选属性的基础上进行增删操作。** 看到构想/uff0c感觉也不是很难/uff0c下面是实际情况/u2014/u2014 实际操作分为三种情况/uff1a 1、选中新属性 2、切换兄弟属性 3、取消已选中属性 第一种情况/uff0c和我们前面的思路吻合/uff0c**只需要将筛选出新属性对应的可选属性集合/uff0c然后与当前的 可选集合 求得 交集即可** 后面两种情况/uff0c都含有一个取消操作/uff08切换兄弟属性、需要取消上一个兄弟属性/uff09/uff0c取消操作/uff0c意味着/uff0c你要把该属性所过滤掉的可选属性/uff0c还回来/uff0c也就是恢复取交集前的 原集合。那问题的关键即为**如何找到这些被该属性过滤掉的可选属性集合或是直接恢复原集合**。 恢复/uff1a可以通过找到所有的原可选集合/uff0c并记录下来/uff0c然后根据取消属性的匹配原集合恢复(这里不能单纯的记录选中操作的可选集合/uff0c因为取消的顺序不一样) 查找过滤掉的可选集合/uff1a操作等同于重新计算可选集合 以上两种方案都不可行。这里就不多做赘述/uff0c实际操作起来/uff0c遍历查询的次数更多了/uff0c不仅达不到优化的效果/uff0c还增加了算法的复杂程度/uff08这里如果小伙伴有更好的想法/uff0c欢迎讨论/uff09。那么最终结论是/uff1a**在对属性有新操作时/uff0c只有新增属性可以基于当前可选属性集合过滤/uff0c其他情况需要重新计算** * ## 优化条件式 如果说上一个优化方案较为笼统的话/uff0c那这里就是整个优化所在的关键了,同时也是**SKUDataFilter** 的核心 认真看了整篇文章就会发现/uff0c整个算法思路的核心/uff0c在于条件式/uff0c不管是查询结果/uff0c还是查询可选属性集合/uff0c实际上都是依赖于条件式的/uff0c我们在查询某个属性时候可选/uff0c实际上是要遍历他所在的条件式列表/uff0c这个列表又要求我们去遍历所有的条件式/uff0c判断这个属性是否在条件式中/uff0c拿到列表之后/uff0c获取非兄弟属性又要遍历这个属性/uff0c是否同时满足所有已选属性的条件式。那么我们整个算法循环次数最多的地方便是**判断某个属性是否存在于某个条件式中** ``` **商品规格 /uff1a 为规格属性加一个坐标/uff0c记录他们的位置 0 1 2 0 F M 1 R G B 2 L X S SKU: 用下标表示条件式 M,G,X - 66元/uff0c10件 --- (1/uff0c1/uff0c1) F,G,S - 88元/uff0c12件 --- (0/uff0c1/uff0c2) F,R,X - 99元/uff0c15件 --- (0/uff0c0/uff0c1) ``` 在上一个例子中/uff0c为每个属性加一个坐标/uff0c如L表示为/uff080, 2/uff09 条件式中用坐标的某一部分表示 这样一来 判断某个属性是否存在于某个条件式/uff1a 正常的操作是遍历条件式中的属性/uff0c分别和该属性做判断(containsObject方法 本质上也是在做遍历) 。 而这里只需要一次判断就够了 /uff0c设**该属性的坐标为/uff08x,y/uff09判断条件式里的第y个值是否等于x即可** /uff08这里的判断取决于条件式存入的是x、还是y/uff09 如 判断M(1,0) 是否在F-G-S(0,1,2)条件式中 即条件式的第0个值是否等于1就OK了/uff08程序猿都是从0开始数的/uff09 比如说/uff0c总共有5个条件式/uff0c每个条件式中有5个属性/uff0c你要找出某个满足某个属性的所有条件式 如果你不去中断遍历/uff0c就要判断25次/uff0c这种方式只需要判定5次就够了/uff0c所以它的优化性实际上是非常高的。 实际上/uff0c真正神奇之处就在于**这样的下标条件式可以清楚的 知道他所拥有的 任何一个属性 的坐标/uff0c进而知道属性的值** # 解决方案-SKUDataFilter --- `SKUDataFilter` 正是基于以上分析和算法优化实现的/uff0c其关键在于`indexPath` 和 `conditionIndexs`。使用`NSIndexPath`记录每个属性的坐标/uff0c使用`conditionIndexs` (条件式下标)中记录的属性`indexPath`的 item, 上面已经详细阐述了他们的原理及作用/uff0c这边再举一个例子来说明`SKUDataFilter`中的用法 ``` 例/uff1a 颜色: r g 尺寸: s m l ``` > `indexPath`记录属性的位置坐标/uff0c表示为 **第`section` 种属性类下面的 第`item`个 属性 /uff08从0计数)** 如上例/uff1a则属性 m 的indexPath表示为 secton : 1, item : 1 > `conditionIndexs` 条件式下标/uff1a记录的属性`indexPath`的 `item` 如上例/uff1a条件 condition (g,l) 使用 `conditionIndexs` 用属性下标表示则为 (1,2)
判断属性是否存在于条件式中/uff0c只需要这样 ```objc conditionIndexs[indexPath.section] == indexPath.row ``` ## 数据通配 `conditionIndexs` 和`indexPath`的结合 为`SKUDataFilter` 不仅算法上取得了优势/uff0c同时也在 数据通配 上起了莫大的作用 不同的后台/uff0c不同的需求/uff0c返回的数据结构都不一样。 然而`SKUDataFilter`真正关心的是属性的坐标/uff0c而不是属性本身的的值/uff0c那么不管你从后台获取的数据结构是怎样的/uff0c也不管你是如何解析的。当然/uff0c你也不需要去关心坐标和条件式下标等等乱七八糟的。你需要做的只是把对应的数据放入对应的代理方法里面去就行了/uff0c不管数据是model/uff0c属性ID、字典还是其他的。 ## 使用说明 使用具体可以参考[SKUDataFilterDemo](https://github.com/SunriseOYR/SKUDataFilter) `SKUDataFilter`最终直接反映的是属性的indexPath, 如果你的属性在UI显示上使用`UICollectionView`实现/uff0c那么`indexPath`是一一对应的/uff0c如果用的循环创建/uff0c找到对应的行和列即可。 1、初始化Filter 并设置代理 ``` - (instancetype)initWithDataSource:(id)dataSource; //当数据更新的时候 重新加载数据 - (void)reloadData; ``` 2、通过代理方法 /uff0c将数据传给`Filter` 以下方法都必需实现/uff0c分别告诉`Filter`/uff0c属性种类个数、每个种类的所有属性/uff08数组/uff09/uff0c条件式个数、每个条件式包含的所有属性、以及每个条件式对应的结果/uff08可以参考本文案例/uff09 ```objc //属性种类个数 - (NSInteger)numberOfSectionsForPropertiesInFilter:(ORSKUDataFilter *)filter; /* * 每个种类所有的的属性值 * 这里不关心具体的值/uff0c可以是属性ID, 属性名/uff0c字典、model */ - (NSArray *)filter:(ORSKUDataFilter *)filter propertiesInSection:(NSInteger)section; //满足条件 的 个数 - (NSInteger)numberOfConditionsInFilter:(ORSKUDataFilter *)filter; /* * 对应的条件式 * 这里条件式的属性值/uff0c需要和filter:propertiesInSection里面的数据 类型保持一致 */ - (NSArray *)filter:(ORSKUDataFilter *)filter conditionForRow:(NSInteger)row; //条件式 对应的 结果数据/uff08库存、价格等/uff09 - (id)filter:(ORSKUDataFilter *)filter resultOfConditionForRow:(NSInteger)row; ``` 3、点击某个属性的时候 把对应属性的`indexPath`传给`Filter` ```objc - (void)didSelectedPropertyWithIndexPath:(NSIndexPath *)indexPath; ``` 4、查询结果(与代理方法resultOfConditionForRow/uff1a对应)-条件不完整会返回nil ```objc //当前结果 @property (nonatomic, strong, readonly) id currentResult; //当前所有可用结果的结果查询/uff0c 一般用于 价格区间 动态变化 @property (nonatomic, strong, readonly) NSSet *currentAvailableResutls; ``` 5、可选属性集合列表、已选属性坐标列表 ```objc //当前 选中的属性indexPath @property (nonatomic, strong, readonly) NSSet *selectedIndexPaths; //当前 可选的属性indexPath @property (nonatomic, strong, readonly) NSSet *availableIndexPathsSet; ``` 6、默认选中第一组SKU ```objc //是否需要默认选中第一组SKU @property (nonatomic, assign) BOOL needDefaultValue; ``` ## 使用注意 1、虽然`SKUDataFilter`不关心具体的值/uff0c但是条件式本质是由属性组成/uff0c故代理方法`filter: propertiesInSection/uff1a`和方法`filter: conditionForRow/uff1a`数据类型应该保持一致 2、因为`SKUDataFilter`关心的是属性的坐标/uff0c那么在代理方法传值的时候/uff0c代理方法`filter: propertiesInSection: `和方法`filter: conditionForRow: `各自的数据顺序要保持一致 并且两个方法的数据也要对应 如本文案例条件式是从上往下/uff08M,G,X/uff09/uff0c传过去的 属性值 也都是从左到右/uff08F、M/uff09-各自保持一致。 同时 条件式为从上到下/uff0c那么`propertiesInSection: ` 也应该是从上到下,先是/uff08F、M/uff09最后是/uff08L、X、S/uff09 实际项目中/uff0c这两种情况发生的概率都非常小/uff0c因为 第一数据统一返回统一解析/uff0c格式99%都是一样。第二数据是从服务器返回/uff0c服务器的数据要进行筛选和过滤/uff0c顺序也不能弄错/uff0c一旦错误/uff0c首先服务器就会出问题 # 更新日志 --- ### 2019.09.15 - swift - 更新swift ### 2019.07.23 - currentAvailableResutls - 加入属性 当前所有可用结果的结果查询/uff0c 一般用于 价格区间 动态变化 ```objc @property (nonatomic, strong, readonly) NSSet *currentAvailableResutls; ``` ### 2019.04.12 - defaultValue - 加入是否默认选中第一组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' - 升级数据防崩溃过滤/uff0c即使`sku-condition`完全对不上号/uff0c也不会闪退了。/uff08针对某些极端测试人员/uff09 ### 2018.06.21 - check - 最近收到很多因为部分sku信息不完整/uff0c导致崩溃的反馈。所以新增了`sku-condition`的检测/uff0c过滤并提示了不完整的`condition`。已更新 ### 2017.12.16 - bug - 由于之前的疏忽/uff0c在更新算法的时候/uff0c漏了一个点/uff0c导致一个非常严重的bug/uff0c感谢简书网友[@毕小强](http://www.jianshu.com/u/1d1454c9bb0b) 指出/uff0c已更新 # 文末 [GitHub传送门](https://github.com/SunriseOYR/SKUDataFilter)