39

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

 4 years ago
source link: https://wanyaxing.com/blog/20190702112829.html?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

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

首先是给数据表加一个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个数据,那就另说了。

Xnip2019-07-02_11-10-49.png

提供一个网页版的中间数计算器: https://wanyaxing.com/middle_value/

提供一个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>

源于随手刷到的 知乎 的一个提问 一个基本的用户排序功能为什么这么难?,虽然是四五年前的问题了,想起自己的确做过这份研究,也好久没更新博客了,所以才有了此篇分享,供大家参考。

原文来自阿星的空间:https://wanyaxing.com/blog/20190702112829.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK