Skip to content

Commit

Permalink
util-core: Implicit conversion from "x.percent" to fractional Double
Browse files Browse the repository at this point in the history
Summary: Problem

It would be useful to have a conversion between "x.percent"
and a fractional Double so that argument passing and usage
example of functions/constructors taking a fraction-as-percentage
argument are more explicit. For example, the user is clear that 50.percent
means 50%, but 0.5 could be 0.5% or 50% and necessitates
reading the documentation.

Solution

Add an implicit "x.percent" conversion to a fractional Double.

JIRA Issues: CSL-5835

Differential Revision: https://phabricator.twitter.biz/D128792
  • Loading branch information
jcrossley authored and jenkins committed Jan 20, 2018
1 parent a750ce7 commit e573d26
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 3 deletions.
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Note that ``PHAB_ID=#`` and ``RB_ID=#`` correspond to associated messages in com

Unreleased

New Features:

* util-core: Added implicit conversion for percentage specified as "x.percent"
to a fractional Double in `c.t.conversions.percent`. ``PHAB_ID=D128792``

18.1.0 2018-01-17

New Features:
Expand Down
37 changes: 37 additions & 0 deletions util-core/src/main/scala/com/twitter/conversions/percent.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.twitter.conversions

import scala.language.implicitConversions

/**
* Implicits for turning x.percent (where x is an Int or Double) into a Double scaled to
* where 1.0 is 100 percent.
*
* @note Negative values, fractional values, and values greater than 100 are permitted.
*
* @example
* {{{
* 1.percent == 0.01
* 100.percent == 1.0
* 99.9.percent == 0.999
* 500.percent == 5.0
* -10.percent == -0.1
* }}}
*/
object percent {

private val BigDecimal100 = BigDecimal(100.0)

class RichDoublePercent private[conversions](val wrapped: Double) extends AnyVal {
// convert wrapped to BigDecimal to preserve precision when dividing Doubles
def percent: Double =
if (wrapped.equals(Double.NaN)
|| wrapped.equals(Double.PositiveInfinity)
|| wrapped.equals(Double.NegativeInfinity)) wrapped
else (BigDecimal(wrapped) / BigDecimal100).doubleValue
}

implicit def intPercentToFractionalDouble(i: Int): RichDoublePercent =
new RichDoublePercent(i)
implicit def doublePercentToFractionalDouble(d: Double): RichDoublePercent =
new RichDoublePercent(d)
}
50 changes: 50 additions & 0 deletions util-core/src/test/scala/com/twitter/conversions/PercentTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.twitter.conversions

import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.Gen
import org.scalatest.FunSuite
import org.scalatest.prop.GeneratorDrivenPropertyChecks

class PercentTest extends FunSuite with GeneratorDrivenPropertyChecks {
import percent._

private[this] val Precision = 0.0000000001

private[this] def doubleEq(d1: Double, d2: Double): Boolean = {
Math.abs(d1 - d2) <= Precision
}

test("percent can be fractional and precision is preserved") {
assert(99.9.percent == 0.999)
assert(99.99.percent == 0.9999)
assert(99.999.percent == 0.99999)
assert(12.3456.percent == 0.123456)
}

test("percent can be > 100") {
assert(101.percent == 1.01)
assert(500.percent == 5.0)
}

test("percent can be < 0") {
assert(-0.1.percent == -0.001)
assert(-500.percent == -5.0)
}

test("assorted percentages") {
forAll(arbitrary[Int]) { i =>
assert(new RichDoublePercent(i).percent == i / 100.0)
}

// We're not as accurate when we get into high double-digit exponents, but that's acceptable.
forAll(Gen.choose(-10000D, 10000D)) { d =>
assert(doubleEq(new RichDoublePercent(d).percent, d / 100.0))
}
}

test("doesn't blow up on edge cases") {
assert(Double.NaN.percent.equals(Double.NaN))
assert(Double.NegativeInfinity.percent.equals(Double.NegativeInfinity))
assert(Double.PositiveInfinity.percent.equals(Double.PositiveInfinity))
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.twitter.conversions

import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner

import com.twitter.util.Duration

@RunWith(classOf[JUnitRunner])
class TimeTest extends FunSuite {
import time._

Expand Down

0 comments on commit e573d26

Please sign in to comment.