设计一个通用的排序方案,关于模糊中间数的计算思路

3,236 阅读4分钟

前言

在很多用户交互的场景中,经常有给用户提供数据排序的需求,排序的交互嘛,前端有各种插件和方案可以实现,问题出在如何保存用户的排序结果。

常规方案

最常规的方案,就是将排序后的所有数据的顺序保存下来或依次重新排序赋值,在数据量小的情况下,这样做可以接受。一旦数据量成千上万,这个方案就比较扯淡了。

所以笔者试图去设计一款通用的排序方案。

通用方案

首先是给数据表加一个double类型的字段用于排序,举个例子:这个字段为rankValue,表名为tableItem。

那么,我们只要设计一个排序接口交给前端,当前端拖拽排序后,分别传出item_id、prev_item_id、next_item_id这三个参数,即可用于重新排序。

顾名思义这对应的逻辑是:将item_id 拖拽到 prev_item_id 和 next_item_id 之间时,重算item_id对应的rankValue值,使其符合上下行排序值之间。

那么,这里有个核心问题来了:如何计算出两个rankValue之间的中间值?

模糊中间数

计算两个数字的中间值,第一想法会是直接 (prevRankValue + nextRankValue) / 2 完事儿。

做当然能做,(10+21)/2=15.5 看着好像是那么回事,但要是再拖拽几下:

(10+15.5)/2  = 12.75
(10+12.75)/2 = 11.375

... 这就会往奇怪的情况去了,强迫症患者不能忍,所以我们需要做一个算法实现这样的效果:

(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…