Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helpers for manipulating Range and NumericRange #18

Merged
merged 1 commit into from
Aug 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,26 @@ class CoreModule(val crossScalaVersion: String) extends CommonModule {

object collections extends Cross[CollectionsModule](Scala12, Scala13)
class CollectionsModule(val crossScalaVersion: String) extends CommonModule {
override def ivyDeps: T[Agg[Dep]] = super.ivyDeps() ++ Agg(CatsCore)

object test extends Tests with CommonTestModule {
override def moduleDeps: Seq[JavaModule] =
super.moduleDeps ++ Seq(scalacheck(crossScalaVersion))

override def crossScalaVersion: String = outerCrossScalaVersion

override def ivyDeps: T[Agg[Dep]] = super.ivyDeps() ++ Agg.from(WordSpec)
override def ivyDeps: T[Agg[Dep]] = super.ivyDeps() ++ Agg.from(WordSpec ++ PropSpec)
}
}

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

override def moduleDeps: Seq[PublishModule] = super.moduleDeps ++ Seq(core(crossScalaVersion))
override def moduleDeps: Seq[PublishModule] = super.moduleDeps ++ Seq(
core(crossScalaVersion),
collections(crossScalaVersion)
)

object test extends Tests with CommonTestModule {
override def crossScalaVersion: String = outerCrossScalaVersion
Expand Down
151 changes: 151 additions & 0 deletions collections/src/peschke/collections/range/RangeUtils.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package peschke.collections.range

import scala.collection.immutable.NumericRange

object RangeUtils {

private def copy
(range: Range)
(start: Int = range.start, end: Int = range.end, step: Int = range.step) // scalafix:ok DisableSyntax.defaultArgs
: Range =
if (range.isInclusive) Range.inclusive(start, end, step)
else Range(start, end, step)

private def copyNumeric[N]
(range: NumericRange[N])
(start: N = range.start, end: N = range.end, step: N = range.step) // scalafix:ok DisableSyntax.defaultArgs
(implicit I: Integral[N])
: NumericRange[N] =
if (range.isInclusive) NumericRange.inclusive(start, end, step)
else NumericRange(start, end, step)

/** The inverse of [[Range.drop]]
*
* Extends the range start such that `range.grow(n).drop(n) == range`
*/
def grow(range: Range, steps: Int): Range =
if (steps <= 0) range
else copy(range)(start = range.start - (range.step * steps))

/** The inverse of [[Range.dropRight]]
*
* Extends the range end such that `range.growRight(n).dropRight(n) == range`
*/
def growRight(range: Range, steps: Int): Range =
if (steps <= 0) range
else copy(range)(end = range.end + (range.step * steps))

/** Shift the entire range by a number of steps
*
* Equivalent to `range.growRight(n).drop(n)`
*
* Note: with the exception of situations where intermediate values would
* overflow or underflow, [[shift]] and [[unshift]] are inverse operations
*/
def shift(range: Range, steps: Int): Range =
if (steps <= 0) range
else {
val offset = range.step * steps
copy(range)(
start = range.start + offset,
end = range.end + offset
)
}

/** Shift the entire range by a number of steps
*
* Equivalent to `range.grow(n).dropRight(n)`, and the inverse of [[shift]]
*/
def unshift(range: Range, steps: Int): Range =
if (steps <= 0) range
else {
val offset = range.step * steps
copy(range)(
start = range.start - offset,
end = range.end - offset
)
}

/** Specialization of [[NumericRange.slice]], returning a [[NumericRange]]
* instead of an [[IndexedSeq]]
*
* Equivalent to `r.drop(from).take(until - from)`
*/
def sliceNumeric[N](range: NumericRange[N], from: Int, until: Int)
(implicit I: Integral[N])
: NumericRange[N] =
if (from <= 0) range.take(until)
else if (until >= range.length) range.drop(from)
else {
val fromValue = I.plus(range.start, I.times(range.step, I.fromInt(from)))

def untilValue =
I.plus(range.start, I.times(range.step, I.fromInt(until - 1)))

if (from >= until) NumericRange(fromValue, fromValue, range.step)
else NumericRange.inclusive(fromValue, untilValue, range.step)
}

/** The inverse of [[NumericRange.drop]]
*
* Extends the range start such that `range.grow(n).drop(n) == range`
*/
def growNumeric[N](range: NumericRange[N], steps: Int)
(implicit I: Integral[N])
: NumericRange[N] =
if (steps <= 0) range
else
copyNumeric(range)(start =
I.minus(range.start, I.times(range.step, I.fromInt(steps)))
)

/** The inverse of [[NumericRange.dropRight]]
*
* Extends the range end such that `range.growRight(n).dropRight(n) == range`
*/
def growNumericRight[N](range: NumericRange[N], steps: Int)
(implicit I: Integral[N])
: NumericRange[N] =
if (steps <= 0) range
else
copyNumeric(range)(end =
I.plus(range.end, I.times(range.step, I.fromInt(steps)))
)

/** Shift the entire range by a number of steps
*
* Equivalent to `range.drop(n).growRight(n)`
*
* Note: with the exception of situations where intermediate values would
* overflow or underflow, [[shiftNumeric]] and [[unshiftNumeric]] are inverse
* operations
*/
def shiftNumeric[N](range: NumericRange[N], steps: Int)
(implicit I: Integral[N])
: NumericRange[N] =
if (steps <= 0) range
else {
val offset = I.times(range.step, I.fromInt(steps))
copyNumeric(range)(
start = I.plus(range.start, offset),
end = I.plus(range.end, offset)
)
}

/** Shift the entire range by a number of steps
*
* Equivalent to `range.dropRight(n).grow(n)`, and the inverse of
* [[shiftNumeric]]
*/
def unshiftNumeric[N](range: NumericRange[N], steps: Int)
(implicit I: Integral[N])
: NumericRange[N] =
if (steps <= 0) range
else {
val offset = I.times(range.step, I.fromInt(steps))
copyNumeric(range)(
start = I.minus(range.start, offset),
end = I.minus(range.end, offset)
)
}
}
84 changes: 84 additions & 0 deletions collections/src/peschke/collections/range/syntax.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package peschke.collections.range

import scala.collection.immutable.NumericRange

object syntax {
implicit final class ScalaCommonsRangeOps(private val range: Range)
extends AnyVal {

/** The inverse of [[Range.drop]]
*
* @see
* [[RangeUtils.grow]]
*/
def grow(steps: Int): Range = RangeUtils.grow(range, steps)

/** The inverse of [[Range.dropRight]]
*
* @see
* [[RangeUtils.growRight]]
*/
def growRight(steps: Int): Range = RangeUtils.growRight(range, steps)

/** Shift the entire range by a number of steps
*
* @see
* [[RangeUtils.shift]]
*/
def shift(steps: Int): Range = RangeUtils.shift(range, steps)

/** Shift the entire range by a number of steps
*
* @see
* [[RangeUtils.unshift]]
*/
def unshift(steps: Int): Range = RangeUtils.unshift(range, steps)
}

implicit final class ScalaCommonsNumericRangeOps[N]
(private val range: NumericRange[N])
extends AnyVal {

/** Specialization of [[NumericRange.slice]], returning a [[NumericRange]]
* instead of an [[IndexedSeq]]
*
* @see
* [[RangeUtils.shiftNumeric]]
*/
def sliceRange(from: Int, until: Int)(implicit I: Integral[N])
: NumericRange[N] =
RangeUtils.sliceNumeric(range, from, until)

/** The inverse of [[NumericRange.drop]]
*
* @see
* [[RangeUtils.growNumeric]]
*/
def grow(steps: Int)(implicit I: Integral[N]): NumericRange[N] =
RangeUtils.growNumeric(range, steps)

/** The inverse of [[NumericRange.dropRight]]
*
* @see
* [[RangeUtils.growNumericRight]]
*/
def growRight(steps: Int)(implicit I: Integral[N]): NumericRange[N] =
RangeUtils.growNumericRight(range, steps)

/** Shift the entire range by a number of steps
*
* @see
* [[RangeUtils.shiftNumeric]]
*/
def shift(steps: Int)(implicit I: Integral[N]): NumericRange[N] =
RangeUtils.shiftNumeric(range, steps)

/** Shift the entire range by a number of steps
*
* @see
* [[RangeUtils.unshiftNumeric]]
*/
def unshift(steps: Int)(implicit I: Integral[N]): NumericRange[N] =
RangeUtils.unshiftNumeric(range, steps)
}
}
12 changes: 12 additions & 0 deletions collections/test/src/peschke/UnitSpec.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
package peschke

import org.scalacheck.Shrink
import org.scalatest.EitherValues
import org.scalatest.OptionValues
import org.scalatest.matchers.must.Matchers
import org.scalatest.propspec.AnyPropSpec
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

trait UnitSpec
extends AnyWordSpec
with Matchers
with EitherValues
with OptionValues

trait PropSpec
extends AnyPropSpec
with Matchers
with EitherValues
with OptionValues
with ScalaCheckDrivenPropertyChecks {
implicit def noShrink[A]: Shrink[A] = Shrink.shrinkAny
}
Loading