PHP中数值范围按指定步长和等分数进行精确分割的教程
在PHP开发过程中,我们经常会遇到需要将一个数值范围按照特定规则进行分割的场景,比如生成等间隔的时间点、划分数值区间、生成测试数据等。本文将详细介绍两种常见的分割方式:按指定步长分割和按指定等分数分割,每种方式都会提供完整的实现代码和使用示例。
一、按指定步长分割数值范围
按指定步长分割的核心逻辑是:从起始值开始,每次累加步长,直到累加结果超过终止值,将每次累加前的结果存入数组,最终得到分割后的所有数值。
需要注意处理浮点数精度问题,避免因为浮点运算误差导致结果不符合预期。下面的函数实现了这个功能,支持整数和浮点数范围,同时也可以选择是否包含终止值。
<?php
/**
* 按指定步长分割数值范围
* @param float $start 起始值
* @param float $end 终止值
* @param float $step 步长,必须为正数
* @param bool $includeEnd 是否包含终止值,默认不包含
* @return array 分割后的数值数组
*/
function splitByStep(float $start, float $end, float $step, bool $includeEnd = false): array {
// 步长必须为正数,否则抛出异常
if ($step <= 0) {
throw new InvalidArgumentException('步长必须为正数');
}
$result = [];
// 如果起始值大于终止值,说明是递减范围,需要调整步长为负数处理?这里先默认处理递增场景,递减场景可以扩展
// 本文先处理常规的起始值小于等于终止值的场景
if ($start > $end) {
// 如果起始值大于终止值,直接返回空数组或者抛异常,根据需求调整,这里返回空数组
return $result;
}
$current = $start;
// 使用精度容差处理浮点数比较,避免精度问题
$epsilon = 0.0000001;
while (true) {
// 如果当前值已经超过终止值,退出循环
if ($current > $end + $epsilon) {
break;
}
$result[] = $current;
// 下次累加步长
$next = $current + $step;
// 如果下一个值已经超过终止值,且不需要包含终止值,退出循环
if ($next > $end + $epsilon && !$includeEnd) {
break;
}
$current = $next;
}
// 如果需要包含终止值,且最后一个值不是终止值,手动添加终止值
if ($includeEnd && end($result) != $end) {
$result[] = $end;
}
return $result;
}
// 使用示例1:整数范围,步长为2,不包含终止值
$range1 = splitByStep(1, 10, 2);
print_r($range1);
// 输出:Array ( [0] => 1 [1] => 3 [2] => 5 [3] => 7 [9] => 9 )
// 使用示例2:浮点数范围,步长为0.5,包含终止值
$range2 = splitByStep(0.0, 2.0, 0.5, true);
print_r($range2);
// 输出:Array ( [0] => 0 [1] => 0.5 [2] => 1 [3] => 1.5 [4] => 2 )
?>上面的代码首先做了参数校验,确保步长为正数,然后循环累加步长,同时使用$epsilon作为浮点数比较的容差,避免精度问题。如果需要包含终止值,最后会判断数组最后一个元素是否为终止值,若不是则手动添加。
二、按指定等分数分割数值范围
按指定等分数分割的逻辑是:先计算整个数值范围的总长度,再用总长度除以等分数得到每个区间的步长,最后从起始值开始每次累加步长得到分割点。这种方式可以保证分割出来的区间数量等于指定的等分数,每个区间的长度尽可能均匀。
同样需要注意浮点数精度和边界值的处理,下面的函数实现了这个功能,支持指定是否包含起始值和终止值。
<?php
/**
* 按指定等分数分割数值范围
* @param float $start 起始值
* @param float $end 终止值
* @param int $parts 等分数,必须为正整数
* @param bool $includeStart 是否包含起始值,默认包含
* @param bool $includeEnd 是否包含终止值,默认包含
* @return array 分割后的数值数组
*/
function splitByParts(float $start, float $end, int $parts, bool $includeStart = true, bool $includeEnd = true): array {
// 等分数必须为正整数,否则抛出异常
if ($parts <= 0) {
throw new InvalidArgumentException('等分数必须为正整数');
}
$result = [];
// 如果起始值大于终止值,返回空数组
if ($start > $end) {
return $result;
}
// 如果起始值等于终止值,直接返回包含该值的数组
if ($start == $end) {
if ($includeStart || $includeEnd) {
return [$start];
}
return [];
}
// 计算总范围长度
$totalLength = $end - $start;
// 计算每个区间的步长
$step = $totalLength / $parts;
// 浮点数容差
$epsilon = 0.0000001;
// 是否添加起始值
if ($includeStart) {
$result[] = $start;
}
// 循环添加中间的分割点
for ($i = 1; $i < $parts; $i++) {
$point = $start + $step * $i;
// 处理浮点数精度,避免偏差
if (abs($point - $end) < $epsilon) {
$point = $end;
}
$result[] = $point;
}
// 是否添加终止值,避免重复添加
if ($includeEnd && (abs(end($result) - $end) > $epsilon)) {
$result[] = $end;
}
return $result;
}
// 使用示例1:范围1-10,等分为3份,包含起止值
$range3 = splitByParts(1, 10, 3);
print_r($range3);
// 输出:Array ( [0] => 1 [1] => 4 [2] => 7 [3] => 10 )
// 使用示例2:范围0-1,等分为4份,不包含起始值,包含终止值
$range4 = splitByParts(0.0, 1.0, 4, false, true);
print_r($range4);
// 输出:Array ( [0] => 0.25 [1] => 0.5 [2] => 0.75 [3] => 1 )
?>这个函数首先计算总范围长度,再除以等分数得到步长,然后循环生成中间的分割点。如果不需要包含起始值,会跳过第一个点的添加;如果不需要包含终止值,最后也不会添加终止值。同时处理了浮点数精度问题,避免分割点出现偏差。
三、两种方式的对比与选择
两种方式的使用场景有所不同:
- 如果明确知道每个区间的长度(步长),优先选择按步长分割的方式,比如需要生成每隔5分钟一个的时间点,就可以用步长300秒(5分钟)来分割时间范围。
- 如果明确需要将范围分成多少份,优先选择按等分数分割的方式,比如需要将0到100的分数分成10个区间做统计,就可以用等分数10来分割。
两种方式都做了浮点数精度和边界值的处理,可以直接在项目中使用,也可以根据实际需求扩展功能,比如支持递减范围分割、自定义精度容差等。