iOS-SKU商品規格組合算法詳解

寫在前面


本篇文章主要是講 SKU 商品規格組合的 問題、解決思路及算法優化。 最後 將提供一個SKU算法的通配方案 - SKUDataFiltergit

本篇文章分析較爲詳細,針對於對SKU問題不甚瞭解的童鞋。github

不想聽我瞎BB的,這邊是乾貨地址算法

文章最後是 使用說明swift

DEMP效果圖數組

003.gif

關於SKU


維基百科: 最小庫存管理單元(Stock Keeping Unit, SKU)是一個會計學名詞,定義爲庫存管理中的最小可用單元。
最小庫存管理單元就是「單品」 最小庫存單元是指包含特定的天然屬性與社會屬性的商品種類,在零售連鎖門店管理中一般稱爲「單品」。對於一種商品而言,當他的品牌、型號、配置、花色、容量、生產日期、保質期、用途、價格、產地等屬性與其餘商品存在不一樣時,就是一個不一樣的最小存貨單元。bash

通俗來說,一個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

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。因此問題仍是在條件式中。當有多個屬性被選中時,判斷一個非可選屬性的兄弟屬性是否可選,必需要知足全部可選屬性的條件式。那麼總體結論即斷定某個屬性的可選性:該屬性要麼同時知足全部已選屬性的條件式,要麼和已選中的某個屬性是兄弟屬性

算法優化


基於上面的思路,再來講一下算法的優化

  • 下降已選屬性的遍歷

將上訴理論應用到實際代碼中,通常是這樣的,再求可選屬性集合時:每次一個新的屬性操做(選中、取消、切換),都會根據上訴結論 分別爲每個已選屬性的篩選出對應的 可選屬性,而後在作交集。 這樣的話,每次一個新的屬性操做,均可能會把的已選屬性重複查詢一遍。

優化方案構想-每次新的屬性操做,只篩選當前屬性的可選屬性,而後在已選屬性的基礎上進行增刪操做。 看到構想,感受也不是很難,下面是實際狀況—— 實際操做分爲三種狀況:

一、選中新屬性
二、切換兄弟屬性
三、取消已選中屬性

第一種狀況,和咱們前面的思路吻合,只須要將篩選出新屬性對應的可選屬性集合,而後與當前的 可選集合 求得 交集便可

後面兩種狀況,都含有一個取消操做(切換兄弟屬性、須要取消上一個兄弟屬性),取消操做,意味着,你要把該屬性所過濾掉的可選屬性,還回來,也就是恢復取交集前的 原集合。那問題的關鍵即爲如何找到這些被該屬性過濾掉的可選屬性集合或是直接恢復原集合

恢復:能夠經過找到全部的原可選集合,並記錄下來,而後根據取消屬性的匹配原集合恢復(這裏不能單純的記錄選中操做的可選集合,由於取消的順序不同) 查找過濾掉的可選集合:操做等同於從新計算可選集合

以上兩種方案都不可行。這裏就很少作贅述,實際操做起來,遍歷查詢的次數更多了,不只達不到優化的效果,還增長了算法的複雜程度(這裏若是小夥伴有更好的想法,歡迎討論)。那麼最終結論是:在對屬性有新操做時,只有新增屬性能夠基於當前可選屬性集合過濾,其餘狀況須要從新計算

  • 優化條件式

若是說上一個優化方案較爲籠統的話,那這裏就是整個優化所在的關鍵了,同時也是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 正是基於以上分析和算法優化實現的,其關鍵在於indexPathconditionIndexs。使用NSIndexPath記錄每一個屬性的座標,使用conditionIndexs (條件式下標)中記錄的屬性indexPath的 item, 上面已經詳細闡述了他們的原理及做用,這邊再舉一個例子來講明SKUDataFilter中的用法

例:

  顏色: r g
  尺寸: s m l
複製代碼

indexPath記錄屬性的位置座標,表示爲 section 種屬性類下面的 第item個 屬性 (從0計數)

如上例:則屬性 m 的indexPath表示爲 secton : 1, item : 1

conditionIndexs 條件式下標:記錄的屬性indexPathitem

如上例:條件 condition (g,l) 使用 conditionIndexs 用屬性下標表示則爲 (1,2)


判斷屬性是否存在於條件式中,只須要這樣

conditionIndexs[indexPath.section] == indexPath.row
複製代碼

數據通配

conditionIndexsindexPath的結合 爲SKUDataFilter 不只算法上取得了優點,同時也在 數據通配 上起了莫大的做用

不一樣的後臺,不一樣的需求,返回的數據結構都不同。

然而SKUDataFilter真正關心的是屬性的座標,而不是屬性自己的的值,那麼無論你從後臺獲取的數據結構是怎樣的,也無論你是如何解析的。固然,你也不須要去關心座標和條件式下標等等亂七八糟的。你須要作的只是把對應的數據放入對應的代理方法裏面去就好了,無論數據是model,屬性ID、字典仍是其餘的。

使用說明

使用具體能夠參考SKUDataFilterDemo

SKUDataFilter最終直接反映的是屬性的indexPath, 若是你的屬性在UI顯示上使用UICollectionView實現,那麼indexPath是一一對應的,若是用的循環建立,找到對應的行和列便可。

一、初始化Filter 並設置代理

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

//當數據更新的時候 從新加載數據
- (void)reloadData;
複製代碼

二、經過代理方法 ,將數據傳給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;
複製代碼

三、點擊某個屬性的時候 把對應屬性的indexPath傳給Filter

- (void)didSelectedPropertyWithIndexPath:(NSIndexPath *)indexPath;
複製代碼

四、查詢結果(與代理方法resultOfConditionForRow:對應)-條件不完整會返回nil

//當前結果
@property (nonatomic, strong, readonly) id  currentResult;
//當前全部可用結果的結果查詢, 通常用於 價格區間 動態變化
@property (nonatomic, strong, readonly) NSSet *currentAvailableResutls;
複製代碼

五、可選屬性集合列表、已選屬性座標列表

//當前 選中的屬性indexPath
@property (nonatomic, strong, readonly) NSSet <NSIndexPath *> *selectedIndexPaths;
//當前 可選的屬性indexPath
@property (nonatomic, strong, readonly) NSSet <NSIndexPath *> *availableIndexPathsSet;
複製代碼

六、默認選中第一組SKU

//是否須要默認選中第一組SKU
@property (nonatomic, assign) BOOL needDefaultValue;
複製代碼

使用注意

一、雖然SKUDataFilter不關心具體的值,可是條件式本質是由屬性組成,故代理方法filter: propertiesInSection:和方法filter: conditionForRow:數據類型應該保持一致
二、由於SKUDataFilter關心的是屬性的座標,那麼在代理方法傳值的時候,代理方法filter: propertiesInSection:和方法filter: conditionForRow:各自的數據順序要保持一致 而且兩個方法的數據也要對應 如本文案例條件式是從上往下(M,G,X),傳過去的 屬性值 也都是從左到右(F、M)-各自保持一致。 同時 條件式爲從上到下,那麼propertiesInSection: 也應該是從上到下,先是(F、M)最後是(L、X、S)
實際項目中,這兩種狀況發生的機率都很是小,由於 第一數據統一返回統一解析,格式99%都是同樣。第二數據是從服務器返回,服務器的數據要進行篩選和過濾,順序也不能弄錯,一旦錯誤,首先服務器就會出問題

更新日誌


2019.09.15
  • 更新swift
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
  • 支持cocopods 導入

    pod 'SKUDataFilter'
    複製代碼
  • 升級數據防崩潰過濾,即便sku-condition徹底對不上號,也不會閃退了。(針對某些極端測試人員)

2018.06.21 -
  • 最近收到不少由於部分sku信息不完整,致使崩潰的反饋。因此新增了sku-condition的檢測,過濾並提示了不完整的condition。已更新
2017.12.16 -
  • 因爲以前的疏忽,在更新算法的時候,漏了一個點,致使一個很是嚴重的bug,感謝簡書網友@畢小強 指出,已更新

文末

GitHub傳送門

相關文章
相關標籤/搜索