Java 范围比较的推荐姿势

一、背景
在平时工作开发过程中,很容易遇到判断某个值是否在某个范围的场景。
如需要校验某个日期是否在某个范围;需要校验某个版本号是否在某个区间;需要校验某个时间点是否在某个时间段内;判断某个人是否属于某个年龄段;判断某个用户的积分是否属于某个等级的区间等。
前一阵子,技术群里有哥们就提了类似的一个问题:
判断当前时间是否在周期的时间段里面有什么好的办法吗 比如 当前时间是2021-10-1 5:00:00 ,设置的时间段为 2021-9-30 1:00:00 -2021-9-30 18:00:00 周期为1天 。 那么每天的5-18点都在周期的时间段里面。 [合十]
有图有真相

群里也有不少同学表达自己的建议

还有

那么, 有没有比较优雅的判断方式呢?
二、建议
如果大家花点心思就可以对这些问题进行抽象,即所谓的范围就是数学里面的区间概念,是否在某个范围,即是否在该区间。

因此,我们可以定义一个区间,然后封装一个函数,传入某个值(区间上的某个点),返回是否在这个区间范围。
Guava
中提供了
com.google.common.collect.Range
类,就是为了解决这个问题。
同时还提供了一系列相关类如
RangeSet
、
ImmutableRangeSet
,可以帮助我们轻松实现区间合并,区间判断是否有重叠,实现区间的不可变特性等,非常强大,超级推荐。
maven 地址 https://mvnrepository.com/artifact/com.google.guava/guava
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>31.0.1-jreversion>
<version>31.0.1-androidversion>
dependency>
gradle 依赖
dependencies {
// Pick one:
// 1. Use Guava in your implementation only:
implementation("com.google.guava:guava:31.0.1-jre")
// 2. Use Guava types in your public API:
api("com.google.guava:guava:31.0.1-jre")
// 3. Android - Use Guava in your implementation only:
implementation("com.google.guava:guava:31.0.1-android")
// 4. Android - Use Guava types in your public API:
api("com.google.guava:guava:31.0.1-android")
}
使用非常容易,只要需要比较的类型实现了
Comparable
接口,就直接可构造区间,然后拿值判断是否在这个区间中。
源码地址: https://github.com/google/guava
使用范例:
public void test_Closed(){
// 1点20分
LocalTime start = LocalTime.of(1,20);
// 8点12分
LocalTime end = LocalTime.of(8,12);
// 构造闭区间
Range<LocalTime> localTimeRange = Range.closed(start,end);
// 测试
LocalTime time1 = LocalTime.of(6,4);
Assert.assertTrue(localTimeRange.contains(time1));
LocalTime time2 = LocalTime.of(1,20);
Assert.assertTrue(localTimeRange.contains(time2));
LocalTime time3 = LocalTime.of(1,19);
Assert.assertFalse(localTimeRange.contains(time3));
LocalTime time4 = LocalTime.of(9,19);
Assert.assertFalse(localTimeRange.contains(time4));
}
Range
官方的单元测试代码
https://github.com/google/guava/blob/master/guava-tests/test/com/google/common/collect/RangeTest.java
tabnine
Range
代码使用范例
https://www.tabnine.com/code/java/classes/com.google.common.collect.Range

Range
还提供了很多方便的函数,还可以对多区间对象进行区交集(
com.google.common.collect.Range#isConnected
)、并集(
com.google.common.collect.Range#intersection
)等。
此外对于多段区间操作还提供了
com.google.common.collect.RangeSet
类。通过一个类构造出多个区间,然后传入一个值判断是否命中任意一个区间(
com.google.common.collect.RangeSet#contains
),是否和另外一个区间有交集(
com.google.common.collect.RangeSet#intersects
)等,非常方便。
写一个简单的单测:
public void testLocalTIme(){
List<Range<LocalTime>> ranges = new ArrayList<>();
ranges.add(Range.closed(LocalTime.of(1,5),LocalTime.of(1,15)));
ranges.add(Range.closed(LocalTime.of(8,25),LocalTime.of(9,15)));
ranges.add(Range.closed(LocalTime.of(8,55),LocalTime.of(9,35)));
RangeSet<LocalTime> rangeSet = ImmutableRangeSet.unionOf(ranges);
Assert.assertTrue(rangeSet.contains(LocalTime.of(1,8)));
Assert.assertFalse(rangeSet.contains(LocalTime.of(1,3)));
Assert.assertTrue(rangeSet.contains(LocalTime.of(8,58)));
}
详细使用方式,参考: https://www.tabnine.com/code/java/classes/com.google.common.collect.RangeSet

还有很多子类型 如
ImmutableRangeSet
,不可变的 RangeSet 帮助我们编码。
com.google.common.collect.ImmutableRangeSetTest#testSingleBoundedRange
public void testSingleBoundedRange() {
ImmutableRangeSet<Integer> rangeSet = ImmutableRangeSet.of(Range.closedOpen(1, 5));
assertThat(rangeSet.asRanges()).contains(Range.closedOpen(1, 5));
assertTrue(rangeSet.intersects(Range.closed(3, 4)));
assertTrue(rangeSet.intersects(Range.closedOpen(0, 2)));
assertTrue(rangeSet.intersects(Range.closedOpen(3, 7)));
assertTrue(rangeSet.intersects(Range.greaterThan(2)));
assertFalse(rangeSet.intersects(Range.greaterThan(7)));
assertTrue(rangeSet.encloses(Range.closed(3, 4)));
assertTrue(rangeSet.encloses(Range.closedOpen(1, 4)));
assertTrue(rangeSet.encloses(Range.closedOpen(1, 5)));
assertFalse(rangeSet.encloses(Range.greaterThan(2)));
assertTrue(rangeSet.contains(3));
assertFalse(rangeSet.contains(5));
assertFalse(rangeSet.contains(0));