Skip to content

Commit 57b92bf

Browse files
Add helpers for manipulating Range and NumericRange
Mostly growing and shrinking the ranges
1 parent a8427b0 commit 57b92bf

File tree

13 files changed

+758
-33
lines changed

13 files changed

+758
-33
lines changed

build.sc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,26 @@ class CoreModule(val crossScalaVersion: String) extends CommonModule {
107107

108108
object collections extends Cross[CollectionsModule](Scala12, Scala13)
109109
class CollectionsModule(val crossScalaVersion: String) extends CommonModule {
110+
override def ivyDeps: T[Agg[Dep]] = super.ivyDeps() ++ Agg(CatsCore)
111+
110112
object test extends Tests with CommonTestModule {
111113
override def moduleDeps: Seq[JavaModule] =
112114
super.moduleDeps ++ Seq(scalacheck(crossScalaVersion))
113115

114116
override def crossScalaVersion: String = outerCrossScalaVersion
115117

116-
override def ivyDeps: T[Agg[Dep]] = super.ivyDeps() ++ Agg.from(WordSpec)
118+
override def ivyDeps: T[Agg[Dep]] = super.ivyDeps() ++ Agg.from(WordSpec ++ PropSpec)
117119
}
118120
}
119121

120122
object scalacheck extends Cross[ScalaCheckModule](Scala12, Scala13)
121123
class ScalaCheckModule(val crossScalaVersion: String) extends CommonModule {
122124
override def ivyDeps: T[Agg[Dep]] = super.ivyDeps() ++ Agg(ScalaCheck)
123125

124-
override def moduleDeps: Seq[PublishModule] = super.moduleDeps ++ Seq(core(crossScalaVersion))
126+
override def moduleDeps: Seq[PublishModule] = super.moduleDeps ++ Seq(
127+
core(crossScalaVersion),
128+
collections(crossScalaVersion)
129+
)
125130

126131
object test extends Tests with CommonTestModule {
127132
override def crossScalaVersion: String = outerCrossScalaVersion
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package peschke.collections.range
2+
3+
import scala.collection.immutable.NumericRange
4+
5+
object RangeUtils {
6+
7+
private def copy
8+
(range: Range)
9+
(start: Int = range.start, end: Int = range.end, step: Int = range.step) // scalafix:ok DisableSyntax.defaultArgs
10+
: Range =
11+
if (range.isInclusive) Range.inclusive(start, end, step)
12+
else Range(start, end, step)
13+
14+
private def copyNumeric[N]
15+
(range: NumericRange[N])
16+
(start: N = range.start, end: N = range.end, step: N = range.step) // scalafix:ok DisableSyntax.defaultArgs
17+
(implicit I: Integral[N])
18+
: NumericRange[N] =
19+
if (range.isInclusive) NumericRange.inclusive(start, end, step)
20+
else NumericRange(start, end, step)
21+
22+
/** The inverse of [[Range.drop]]
23+
*
24+
* Extends the range start such that `range.grow(n).drop(n) == range`
25+
*/
26+
def grow(range: Range, steps: Int): Range =
27+
if (steps <= 0) range
28+
else copy(range)(start = range.start - (range.step * steps))
29+
30+
/** The inverse of [[Range.dropRight]]
31+
*
32+
* Extends the range end such that `range.growRight(n).dropRight(n) == range`
33+
*/
34+
def growRight(range: Range, steps: Int): Range =
35+
if (steps <= 0) range
36+
else copy(range)(end = range.end + (range.step * steps))
37+
38+
/** Shift the entire range by a number of steps
39+
*
40+
* Equivalent to `range.growRight(n).drop(n)`
41+
*
42+
* Note: with the exception of situations where intermediate values would
43+
* overflow or underflow, [[shift]] and [[unshift]] are inverse operations
44+
*/
45+
def shift(range: Range, steps: Int): Range =
46+
if (steps <= 0) range
47+
else {
48+
val offset = range.step * steps
49+
copy(range)(
50+
start = range.start + offset,
51+
end = range.end + offset
52+
)
53+
}
54+
55+
/** Shift the entire range by a number of steps
56+
*
57+
* Equivalent to `range.grow(n).dropRight(n)`, and the inverse of [[shift]]
58+
*/
59+
def unshift(range: Range, steps: Int): Range =
60+
if (steps <= 0) range
61+
else {
62+
val offset = range.step * steps
63+
copy(range)(
64+
start = range.start - offset,
65+
end = range.end - offset
66+
)
67+
}
68+
69+
/** Specialization of [[NumericRange.slice]], returning a [[NumericRange]]
70+
* instead of an [[IndexedSeq]]
71+
*
72+
* Equivalent to `r.drop(from).take(until - from)`
73+
*/
74+
def sliceNumeric[N](range: NumericRange[N], from: Int, until: Int)
75+
(implicit I: Integral[N])
76+
: NumericRange[N] =
77+
if (from <= 0) range.take(until)
78+
else if (until >= range.length) range.drop(from)
79+
else {
80+
val fromValue = I.plus(range.start, I.times(range.step, I.fromInt(from)))
81+
82+
def untilValue =
83+
I.plus(range.start, I.times(range.step, I.fromInt(until - 1)))
84+
85+
if (from >= until) NumericRange(fromValue, fromValue, range.step)
86+
else NumericRange.inclusive(fromValue, untilValue, range.step)
87+
}
88+
89+
/** The inverse of [[NumericRange.drop]]
90+
*
91+
* Extends the range start such that `range.grow(n).drop(n) == range`
92+
*/
93+
def growNumeric[N](range: NumericRange[N], steps: Int)
94+
(implicit I: Integral[N])
95+
: NumericRange[N] =
96+
if (steps <= 0) range
97+
else
98+
copyNumeric(range)(start =
99+
I.minus(range.start, I.times(range.step, I.fromInt(steps)))
100+
)
101+
102+
/** The inverse of [[NumericRange.dropRight]]
103+
*
104+
* Extends the range end such that `range.growRight(n).dropRight(n) == range`
105+
*/
106+
def growNumericRight[N](range: NumericRange[N], steps: Int)
107+
(implicit I: Integral[N])
108+
: NumericRange[N] =
109+
if (steps <= 0) range
110+
else
111+
copyNumeric(range)(end =
112+
I.plus(range.end, I.times(range.step, I.fromInt(steps)))
113+
)
114+
115+
/** Shift the entire range by a number of steps
116+
*
117+
* Equivalent to `range.drop(n).growRight(n)`
118+
*
119+
* Note: with the exception of situations where intermediate values would
120+
* overflow or underflow, [[shiftNumeric]] and [[unshiftNumeric]] are inverse
121+
* operations
122+
*/
123+
def shiftNumeric[N](range: NumericRange[N], steps: Int)
124+
(implicit I: Integral[N])
125+
: NumericRange[N] =
126+
if (steps <= 0) range
127+
else {
128+
val offset = I.times(range.step, I.fromInt(steps))
129+
copyNumeric(range)(
130+
start = I.plus(range.start, offset),
131+
end = I.plus(range.end, offset)
132+
)
133+
}
134+
135+
/** Shift the entire range by a number of steps
136+
*
137+
* Equivalent to `range.dropRight(n).grow(n)`, and the inverse of
138+
* [[shiftNumeric]]
139+
*/
140+
def unshiftNumeric[N](range: NumericRange[N], steps: Int)
141+
(implicit I: Integral[N])
142+
: NumericRange[N] =
143+
if (steps <= 0) range
144+
else {
145+
val offset = I.times(range.step, I.fromInt(steps))
146+
copyNumeric(range)(
147+
start = I.minus(range.start, offset),
148+
end = I.minus(range.end, offset)
149+
)
150+
}
151+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package peschke.collections.range
2+
3+
import scala.collection.immutable.NumericRange
4+
5+
object syntax {
6+
implicit final class ScalaCommonsRangeOps(private val range: Range)
7+
extends AnyVal {
8+
9+
/** The inverse of [[Range.drop]]
10+
*
11+
* @see
12+
* [[RangeUtils.grow]]
13+
*/
14+
def grow(steps: Int): Range = RangeUtils.grow(range, steps)
15+
16+
/** The inverse of [[Range.dropRight]]
17+
*
18+
* @see
19+
* [[RangeUtils.growRight]]
20+
*/
21+
def growRight(steps: Int): Range = RangeUtils.growRight(range, steps)
22+
23+
/** Shift the entire range by a number of steps
24+
*
25+
* @see
26+
* [[RangeUtils.shift]]
27+
*/
28+
def shift(steps: Int): Range = RangeUtils.shift(range, steps)
29+
30+
/** Shift the entire range by a number of steps
31+
*
32+
* @see
33+
* [[RangeUtils.unshift]]
34+
*/
35+
def unshift(steps: Int): Range = RangeUtils.unshift(range, steps)
36+
}
37+
38+
implicit final class ScalaCommonsNumericRangeOps[N]
39+
(private val range: NumericRange[N])
40+
extends AnyVal {
41+
42+
/** Specialization of [[NumericRange.slice]], returning a [[NumericRange]]
43+
* instead of an [[IndexedSeq]]
44+
*
45+
* @see
46+
* [[RangeUtils.shiftNumeric]]
47+
*/
48+
def sliceRange(from: Int, until: Int)(implicit I: Integral[N])
49+
: NumericRange[N] =
50+
RangeUtils.sliceNumeric(range, from, until)
51+
52+
/** The inverse of [[NumericRange.drop]]
53+
*
54+
* @see
55+
* [[RangeUtils.growNumeric]]
56+
*/
57+
def grow(steps: Int)(implicit I: Integral[N]): NumericRange[N] =
58+
RangeUtils.growNumeric(range, steps)
59+
60+
/** The inverse of [[NumericRange.dropRight]]
61+
*
62+
* @see
63+
* [[RangeUtils.growNumericRight]]
64+
*/
65+
def growRight(steps: Int)(implicit I: Integral[N]): NumericRange[N] =
66+
RangeUtils.growNumericRight(range, steps)
67+
68+
/** Shift the entire range by a number of steps
69+
*
70+
* @see
71+
* [[RangeUtils.shiftNumeric]]
72+
*/
73+
def shift(steps: Int)(implicit I: Integral[N]): NumericRange[N] =
74+
RangeUtils.shiftNumeric(range, steps)
75+
76+
/** Shift the entire range by a number of steps
77+
*
78+
* @see
79+
* [[RangeUtils.unshiftNumeric]]
80+
*/
81+
def unshift(steps: Int)(implicit I: Integral[N]): NumericRange[N] =
82+
RangeUtils.unshiftNumeric(range, steps)
83+
}
84+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
package peschke
22

3+
import org.scalacheck.Shrink
34
import org.scalatest.EitherValues
45
import org.scalatest.OptionValues
56
import org.scalatest.matchers.must.Matchers
7+
import org.scalatest.propspec.AnyPropSpec
68
import org.scalatest.wordspec.AnyWordSpec
9+
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
710

811
trait UnitSpec
912
extends AnyWordSpec
1013
with Matchers
1114
with EitherValues
1215
with OptionValues
16+
17+
trait PropSpec
18+
extends AnyPropSpec
19+
with Matchers
20+
with EitherValues
21+
with OptionValues
22+
with ScalaCheckDrivenPropertyChecks {
23+
implicit def noShrink[A]: Shrink[A] = Shrink.shrinkAny
24+
}

0 commit comments

Comments
 (0)