Skip to content

Commit

Permalink
util-core: New conversions
Browse files Browse the repository at this point in the history
Problem

Some of the conversions in `c.t.conversions` could benefit from using
`AnyVal`. However, due to ABI breakages this is, wait for it, a tough
conversion to make.

Solution

Introduce new implementations that follow a naming scheme of
`SomethingOps`. Where possible the implementations are `AnyVal` based
avoiding allocations for the common usage pattern. Migrate using
drop-in replacements by updating imports.

Result

Consistency and less allocations for users.

JIRA Issues: CSL-7356

Differential Revision: https://phabricator.twitter.biz/D249403
  • Loading branch information
kevinoliver authored and cacoco committed Dec 6, 2018
1 parent 8919bb2 commit ee56e5f
Show file tree
Hide file tree
Showing 18 changed files with 438 additions and 11 deletions.
26 changes: 26 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,38 @@ New Features
* util-core: Provide a way to listen for stream termination to `c.t.util.Reader`, `Reader#onClose`
which is satisfied when the stream is discarded or read until the end. ``PHAB_ID=D236311``

* util-core: Conversions in `c.t.conversions` have new implementations
that follow a naming scheme of `SomethingOps`. Where possible the implementations
are `AnyVal` based avoiding allocations for the common usage pattern.
``PHAB_ID=D249403``

- `percent` is now `PercentOps`
- `storage` is now `StorageUnitOps`
- `string` is now `StringOps`
- `thread` is now `ThreadOps`
- `time` is now `DurationOps`
- `u64` is now `U64Ops`

Bug Fixes
~~~~~~~~~

* util-core: Fixed a bug where tail would sometimes return Some empty AsyncStream instead of None.
``PHAB_ID=D241513``

Deprecations
~~~~~~~~~~~~

* util-core: Conversions in `c.t.conversions` have been deprecated in favor of `SomethingOps`
versions. Where possible the implementations are `AnyVal` based and use implicit classes
instead of implicit conversions. ``PHAB_ID=D249403``

- `percent` is now `PercentOps`
- `storage` is now `StorageUnitOps`
- `string` is now `StringOps`
- `thread` is now `ThreadOps`
- `time` is now `DurationOps`
- `u64` is now `U64Ops`

Breaking API Changes
~~~~~~~~~~~~~~~~~~~~

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

import com.twitter.util.Duration
import java.util.concurrent.TimeUnit

/**
* Implicits for writing readable [[Duration]]s.
*
* @example
* {{{
* import com.twitter.conversions.DurationOps._
*
* 2000.nanoseconds
* 50.milliseconds
* 1.second
* 24.hours
* 40.days
* }}}
*/
object DurationOps {

implicit class RichLong(val numNanos: Long) extends AnyVal {
def nanoseconds: Duration = Duration(numNanos, TimeUnit.NANOSECONDS)
def nanosecond: Duration = nanoseconds
def microseconds: Duration = Duration(numNanos, TimeUnit.MICROSECONDS)
def microsecond: Duration = microseconds
def milliseconds: Duration = Duration(numNanos, TimeUnit.MILLISECONDS)
def millisecond: Duration = milliseconds
def millis: Duration = milliseconds
def seconds: Duration = Duration(numNanos, TimeUnit.SECONDS)
def second: Duration = seconds
def minutes: Duration = Duration(numNanos, TimeUnit.MINUTES)
def minute: Duration = minutes
def hours: Duration = Duration(numNanos, TimeUnit.HOURS)
def hour: Duration = hours
def days: Duration = Duration(numNanos, TimeUnit.DAYS)
def day: Duration = days
}

}
33 changes: 33 additions & 0 deletions util-core/src/main/scala/com/twitter/conversions/PercentOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.twitter.conversions

/**
* 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
* {{{
* import com.twitter.conversions.PercentOps._
*
* 1.percent == 0.01
* 100.percent == 1.0
* 99.9.percent == 0.999
* 500.percent == 5.0
* -10.percent == -0.1
* }}}
*/
object PercentOps {

private val BigDecimal100 = BigDecimal(100.0)

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.twitter.conversions

import com.twitter.util.StorageUnit

/**
* Implicits for writing readable [[StorageUnit]]s.
*
* @example
* {{{
* import com.twitter.conversions.StorageUnitOps._
*
* 5.bytes
* 1.kilobyte
* 256.gigabytes
* }}}
*/
object StorageUnitOps {

implicit class RichLong(val numBytes: Long) extends AnyVal {
def byte: StorageUnit = bytes
def bytes: StorageUnit = StorageUnit.fromBytes(numBytes)
def kilobyte: StorageUnit = kilobytes
def kilobytes: StorageUnit = StorageUnit.fromKilobytes(numBytes)
def megabyte: StorageUnit = megabytes
def megabytes: StorageUnit = StorageUnit.fromMegabytes(numBytes)
def gigabyte: StorageUnit = gigabytes
def gigabytes: StorageUnit = StorageUnit.fromGigabytes(numBytes)
def terabyte: StorageUnit = terabytes
def terabytes: StorageUnit = StorageUnit.fromTerabytes(numBytes)
def petabyte: StorageUnit = petabytes
def petabytes: StorageUnit = StorageUnit.fromPetabytes(numBytes)

def thousand: Long = numBytes * 1000
def million: Long = numBytes * 1000 * 1000
def billion: Long = numBytes * 1000 * 1000 * 1000
}
}
144 changes: 144 additions & 0 deletions util-core/src/main/scala/com/twitter/conversions/StringOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.twitter.conversions

import scala.util.matching.Regex

object StringOps {

// we intentionally don't unquote "\$" here, so it can be used to escape interpolation later.
private val UNQUOTE_RE = """\\(u[\dA-Fa-f]{4}|x[\dA-Fa-f]{2}|[/rnt\"\\])""".r

private val QUOTE_RE = "[\u0000-\u001f\u007f-\uffff\\\\\"]".r

implicit final class RichString(val string: String) extends AnyVal {

/**
* For every section of a string that matches a regular expression, call
* a function to determine a replacement (as in python's
* `re.sub`). The function will be passed the Matcher object
* corresponding to the substring that matches the pattern, and that
* substring will be replaced by the function's result.
*
* For example, this call:
* {{{
* "ohio".regexSub("""h.""".r) { m => "n" }
* }}}
* will return the string `"ono"`.
*
* The matches are found using `Matcher.find()` and so
* will obey all the normal java rules (the matches will not overlap,
* etc).
*
* @param re the regex pattern to replace
* @param replace a function that takes Regex.MatchData objects and
* returns a string to substitute
* @return the resulting string with replacements made
*/
def regexSub(re: Regex)(replace: Regex.MatchData => String): String = {
var offset = 0
val out = new StringBuilder()

for (m <- re.findAllIn(string).matchData) {
if (m.start > offset) {
out.append(string.substring(offset, m.start))
}

out.append(replace(m))
offset = m.end
}

if (offset < string.length) {
out.append(string.substring(offset))
}
out.toString
}

/**
* Quote a string so that unprintable chars (in ASCII) are represented by
* C-style backslash expressions. For example, a raw linefeed will be
* translated into `"\n"`. Control codes (anything below 0x20)
* and unprintables (anything above 0x7E) are turned into either
* `"\xHH"` or `"\\uHHHH"` expressions, depending on
* their range. Embedded backslashes and double-quotes are also quoted.
*
* @return a quoted string, suitable for ASCII display
*/
def quoteC(): String = {
regexSub(QUOTE_RE) { m =>
m.matched.charAt(0) match {
case '\r' => "\\r"
case '\n' => "\\n"
case '\t' => "\\t"
case '"' => "\\\""
case '\\' => "\\\\"
case c =>
if (c <= 255) {
"\\x%02x".format(c.asInstanceOf[Int])
} else {
"\\u%04x" format c.asInstanceOf[Int]
}
}
}
}

/**
* Unquote an ASCII string that has been quoted in a style like
* [[quoteC()]] and convert it into a standard unicode string.
* `"\\uHHHH"` and `"\xHH"` expressions are unpacked
* into unicode characters, as well as `"\r"`, `"\n"`,
* `"\t"`, `"\\"`, and `'\"'`.
*
* @return an unquoted unicode string
*/
def unquoteC(): String = {
regexSub(UNQUOTE_RE) { m =>
val ch = m.group(1).charAt(0) match {
// holy crap! this is terrible:
case 'u' =>
Character.valueOf(Integer.valueOf(m.group(1).substring(1), 16).asInstanceOf[Int].toChar)
case 'x' =>
Character.valueOf(Integer.valueOf(m.group(1).substring(1), 16).asInstanceOf[Int].toChar)
case 'r' => '\r'
case 'n' => '\n'
case 't' => '\t'
case x => x
}
ch.toString
}
}

/**
* Turn a string of hex digits into a byte array. This does the exact
* opposite of `Array[Byte]#hexlify`.
*/
def unhexlify(): Array[Byte] = {
val buffer = new Array[Byte]((string.length + 1) / 2)
string.grouped(2).toSeq.zipWithIndex.foreach {
case (substr, i) =>
buffer(i) = Integer.parseInt(substr, 16).toByte
}
buffer
}
}

def hexlify(array: Array[Byte], from: Int, to: Int): String = {
val out = new StringBuffer
for (i <- from until to) {
val b = array(i)
val s = (b.toInt & 0xff).toHexString
if (s.length < 2) {
out.append('0')
}
out.append(s)
}
out.toString
}

implicit final class RichByteArray(val bytes: Array[Byte]) extends AnyVal {

/**
* Turn an `Array[Byte]` into a string of hex digits.
*/
def hexlify: String = StringOps.hexlify(bytes, 0, bytes.length)
}

}
19 changes: 19 additions & 0 deletions util-core/src/main/scala/com/twitter/conversions/ThreadOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.twitter.conversions

import java.util.concurrent.Callable
import scala.language.implicitConversions

/**
* Implicits for turning a block of code into a Runnable or Callable.
*/
object ThreadOps {

implicit def makeRunnable(f: => Unit): Runnable = new Runnable() {
def run(): Unit = f
}

implicit def makeCallable[T](f: => T): Callable[T] = new Callable[T]() {
def call(): T = f
}

}
22 changes: 22 additions & 0 deletions util-core/src/main/scala/com/twitter/conversions/U64Ops.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.twitter.conversions

object U64Ops {

/**
* Parses this HEX string as an unsigned 64-bit long value. Be careful, this can throw
* [[NumberFormatException]].
*
* @see [[java.lang.Long.parseUnsignedLong()]]
*/
implicit class StringOps(val self: String) extends AnyVal {
def toU64Long: Long = java.lang.Long.parseUnsignedLong(self, 16)
}

/**
* Converts this unsigned 64-bit long value into a 16-character HEX string.
*/
implicit class LongOps(val self: Long) extends AnyVal {
def toU64HexString: String = "%016x".format(self)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import scala.language.implicitConversions
* -10.percent == -0.1
* }}}
*/
@deprecated("Use the AnyVal version `com.twitter.conversions.PercentOps`", "2018-12-05")
object percent {

private val BigDecimal100 = BigDecimal(100.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.twitter.conversions
import com.twitter.util.StorageUnit
import scala.language.implicitConversions

@deprecated("Use the AnyVal version `com.twitter.conversions.StorageUnitOps`", "2018-12-05")
object storage {
class RichWholeNumber(wrapped: Long) {
def byte: StorageUnit = bytes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.twitter.conversions
import scala.language.implicitConversions
import scala.util.matching.Regex

@deprecated("Use the AnyVal version `com.twitter.conversions.StringOps`", "2018-12-05")
object string {
final class RichString(wrapped: String) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import scala.language.implicitConversions
/**
* Implicits for turning a block of code into a Runnable or Callable.
*/
@deprecated("Use `com.twitter.conversions.ThreadOps`", "2018-12-05")
object thread {
implicit def makeRunnable(f: => Unit): Runnable = new Runnable() { def run(): Unit = f }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.twitter.util.Duration
import java.util.concurrent.TimeUnit
import scala.language.implicitConversions

@deprecated("Use the AnyVal version `com.twitter.conversions.DurationOps`", "2018-12-05")
object time {
class RichWholeNumber(wrapped: Long) {
def nanoseconds: Duration = Duration(wrapped, TimeUnit.NANOSECONDS)
Expand Down
1 change: 1 addition & 0 deletions util-core/src/main/scala/com/twitter/conversions/u64.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.twitter.conversions

@deprecated("Use `com.twitter.conversions.U64Ops`", "2018-12-05")
object u64 {

/**
Expand Down
Loading

0 comments on commit ee56e5f

Please sign in to comment.