設計一個通用的排序方案,關於模糊中間數的計算思路

前言

在不少用戶交互的場景中,常常有給用戶提供數據排序的需求,排序的交互嘛,前端有各類插件和方案能夠實現,問題出在如何保存用戶的排序結果。javascript

常規方案

最常規的方案,就是將排序後的全部數據的順序保存下來或依次從新排序賦值,在數據量小的狀況下,這樣作能夠接受。一旦數據量成千上萬,這個方案就比較扯淡了。php

因此筆者試圖去設計一款通用的排序方案。html

通用方案

首先是給數據表加一個double類型的字段用於排序,舉個例子:這個字段爲rankValue,表名爲tableItem。前端

那麼,咱們只要設計一個排序接口交給前端,當前端拖拽排序後,分別傳出item_id、prev_item_id、next_item_id這三個參數,便可用於從新排序。java

顧名思義這對應的邏輯是:將item_id 拖拽到 prev_item_id 和 next_item_id 之間時,重算item_id對應的rankValue值,使其符合上下行排序值之間。算法

那麼,這裏有個核心問題來了:如何計算出兩個rankValue之間的中間值?數據庫

模糊中間數

計算兩個數字的中間值,第一想法會是直接 (prevRankValue + nextRankValue) / 2 完事兒。api

作固然能作,(10+21)/2=15.5 看着好像是那麼回事,但要是再拖拽幾下:函數

(10+15.5)/2  = 12.75
(10+12.75)/2 = 11.375
複製代碼

... 這就會往奇怪的狀況去了,強迫症患者不能忍,因此咱們須要作一個算法實現這樣的效果:post

(10+15.5)/2  ≈ 13
(10+12.75)/2 ≈ 11
複製代碼

我稱之爲模糊中間數,即取得兩個數字的模糊中間數,並儘量的忽略精確值。好比0.99與1.2的中間數是1,9000與1002的中間數是950而不是951。

之因此這樣作,就是爲了使咱們的數據在大量的拖拽調序以後,它們對應的排序值不要變得太難看,固然你硬是要在1和2之間插100個數據,那就另說了。

核心算法

提供一個PHP版本的代碼,供參考:

<?php
/** * 數學相關處理函數庫文件 * @package W2 * @author wanyaxing * @since 1.0 * @version 1.0 */
class W2Math {

    /** 取得數字的精確位,正數表示n位小數,負數表示精確到個十百千萬位(10的(n-1)次方) */
    public static function getPrecisionOfNumber($number) {
        $number = abs($number);
        $len = strlen($number);
        if (strpos($number,'.')>=0)
        {
            return $len - strpos($number,'.') + 1;
        }
        else
        {
            for ($i=1; $i < $len; $i++)
            {
                if (substr($number,$len-$i,1) > 0)
                {
                    return 0 - ($i - 1);
                }
            }
        }
    }

    /** * 取得兩個數字的模糊中間數,並儘量的忽略精確值。好比0.99與1.2的中間數是1 * @param [type] $bigNumber [description] * @param [type] $smallNumber [description] * @param boolean $isShortIfShortAble [description] * @return [type] [description] */
    public static function getMiddleBetweenNumbers($bigNumber=null,$smallNumber=null) {
        if (!is_null($bigNumber) || !is_null($smallNumber))
        {
            if (is_null($bigNumber))
            {
                $precision = min(W2Math::getPrecisionOfNumber($smallNumber),-1);
                return $smallNumber + pow(10,abs($precision));
            }
            else if (is_null($smallNumber))
            {
                $precision = min(W2Math::getPrecisionOfNumber($bigNumber),-1);
                return $bigNumber - pow(10,abs($precision));
            }
            else if ($bigNumber==$smallNumber)
            {
                return $bigNumber;
            }
            else if ($bigNumber<$smallNumber)
            {
                return null;
            }
            else
            {
                $middle = $smallNumber + (($bigNumber - $smallNumber)/2);
                $precisionMin = min(W2Math::getPrecisionOfNumber($bigNumber),W2Math::getPrecisionOfNumber($smallNumber),-1);
                $precisionMax = max(W2Math::getPrecisionOfNumber($bigNumber),W2Math::getPrecisionOfNumber($smallNumber),$precisionMin);
                for ($i=$precisionMin; $i <=$precisionMax ; $i++) {
                    $tmp = round($middle,$i);
                    if ($tmp>$smallNumber && $tmp<$bigNumber)
                    {
                        return $tmp;
                    }
                }
            }
        }
        return null;
    }
}
複製代碼

注意,在這裏getMiddleBetweenNumbers方法接受的參數是嚴格要求大數字在前小數字在後的,你應該在傳參以前的業務邏輯中保證這個數字順序,固然,此處邏輯僅供參考,你們也能夠直接改代碼,實現兼容方案,這就看你們業務需求了。

接口實現

繼續提供一份接口方案的核心代碼,供你們參考:

<?php
// ItemHandler.php
class ItemHandler extends AbstractHandler {
    /** 取得兩個商品排序的中間排序值 */
    public static function getRankValueBetweenItems($prevItemID=null,$nextItemID=null) {
        $prevItemModel = ItemHandler::loadModelById($prevItemID);
        $nextItemModel = ItemHandler::loadModelById($nextItemID);
        if (is_object($prevItemModel) || is_object($nextItemModel) )
        {
            if (!is_object($prevItemModel))
            {
                $prevItemModel = ItemHandler::loadModelFirstInList(array('rankValue > ' . $nextItemModel->getRankValue(),'status'=>$nextItemModel->getStatus()),'rankValue asc',1,1);
            }
            if (!is_object($nextItemModel))
            {
                $nextItemModel = ItemHandler::loadModelFirstInList(array('rankValue < ' . $prevItemModel->getRankValue(),'status'=>$prevItemModel->getStatus()),'rankValue desc',1,1);
            }
            $bigNumber     = is_object($prevItemModel)?$prevItemModel->getRankValue():null;
            $smallNumber   = is_object($nextItemModel)?$nextItemModel->getRankValue():null;
            return W2Math::getMiddleBetweenNumbers($bigNumber,$smallNumber);
        }
        return null;
    }
}
複製代碼

能夠在此處看到,prev_item_id 和 next_item_id 二者並不都是必傳參數,只傳一個參數也能夠,在接口裏能夠嘗試從數據庫裏取出另外一個數據,若是取不到數也能夠的,那就是當你想要將某數據拖拽到整個護具的第一行或最後一行時。

<?php
// ItemController.php
class ItemController extends AbstractController{
    public static function save($tmpModel,$isAdd=false) {

        if ($tmpModel->isProperyModified('status') && $tmpModel->properyValue('status')==STATUS_NORMAL)
        {
            $tmpModel->setRankValue(time());
        }

        return parent::save($tmpModel,$isAdd);
    }

    public static function actionResetRankValueOfItem() {
        if (static::getAuthIfUserCanDoIt(Utility::getCurrentUserID(),'axapi',null) != 'admin')
        {
            return HaoResult::init(ERROR_CODE::$NO_AUTH);
        }

        $itemID = W2HttpRequest::getRequestInt('item_id');
        $itemModel = ItemHandler::loadModelById($itemID);
        if (!is_object($itemModel))
        {
            return HaoResult::init(ERROR_CODE::$DATA_EMPTY);
        }
        $prevItemID = W2HttpRequest::getRequestInt('prev_item_id');//上一個(其rankValue值應該更大)
        $nextItemID = W2HttpRequest::getRequestInt('next_item_id');//下一個(其rankValue值應該較小)

        $newRankValue = ItemHandler::getRankValueBetweenItems($prevItemID,$nextItemID);
        $itemModel->setRankValue($newRankValue);

        return static::save($itemModel);
    }
}

複製代碼

上文只是核心代碼,注意其中save方法裏,對於初始化的數據,作了一個 $tmpModel->setRankValue(time())動做,這就是說給初始化的數據,按照時間戳設定排序值,這是一個小技巧,你們也能夠根據業務狀況酌情處理。

前端實現

再提供一份前端實現的核心代碼,供你們參考

  • 在輸出的表格裏,給每行數據綁定item_id
<tbody>
            <?php foreach ($requestResult->results() as $detailResult) : ?>
            <tr item_id="<?= $detailResult->find('id') ?>" >
                <td><?= $detailResult->find('itemName') ?></td>
            </tr>
            <?php endforeach ?>
        </tbody>
複製代碼
  • 使用Sortable.js插件爲表格行提供拖拽功能,在拖拽行爲完成後,取 item_id 調用接口,保存排序結果。
<script type="text/javascript">
    $(function(){
        $LAB
            .script('/third/haouploader/js/sortable/Sortable.js')
            .wait(function(){
                    Sortable.create($('#item_list_bg tbody')[0],{
                            draggable:'tr',
                            animation: 150,
                            // Changed sorting within list
                            onEnd: function (/**Event*/evt) {
                                if (evt.oldIndex != evt.newIndex)
                                {
                                    var $this = $(evt.item);
                                    var params = {};
                                    params['item_id'] = $this.attr('item_id');
                                    params['prev_item_id'] = $this.prev().attr('item_id');
                                    params['next_item_id'] = $this.next().attr('item_id');
                                    HaoConnect.post('item/reset_rank_value_of_item',params).then(function(result){
                                        if (result.isResultsOK())
                                        {
                                            console.log('拖拽排序結果保存成功');
                                        }
                                    });
                                }
                            }
                        }
                    );
                    resetMenuDiv(result.find('menu'));
                });
    });
</script>
複製代碼

後語

源於隨手刷到的 知乎 的一個提問 一個基本的用戶排序功能爲何這麼難?,雖然是四五年前的問題了,想起本身的確作過這份研究,也很久沒更新博客了,因此纔有了此篇分享,供你們參考。

原文來自阿星的博客: wanyaxing.com/blog/201907…

相關文章
相關標籤/搜索