设计一个通用的排序方案,关于模糊中间数的计算思路
前言
在很多用户交互的场景中,经常有给用户提供数据排序的需求,排序的交互嘛,前端有各种插件和方案可以实现,问题出在如何保存用户的排序结果。
常规方案
最常规的方案,就是将排序后的所有数据的顺序保存下来或依次重新排序赋值,在数据量小的情况下,这样做可以接受。一旦数据量成千上万,这个方案就比较扯淡了。
所以笔者试图去设计一款通用的排序方案。
通用方案
首先是给数据表加一个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个数据,那就另说了。
提供一个网页版的中间数计算器: 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>
后语
源于随手刷到的 知乎 的一个提问 一个基本的用户排序功能为什么这么难?,虽然是四五年前的问题了,想起自己的确做过这份研究,也好久没更新博客了,所以才有了此篇分享,供大家参考。