-
Notifications
You must be signed in to change notification settings - Fork 0
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 duration and byte-count formatters #188
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package io.flow.util | ||
|
||
import java.math.RoundingMode | ||
import java.text.DecimalFormat | ||
|
||
object ByteFormatter { | ||
def byteCountSI(bytes: Long): String = { | ||
format(bytes, true) | ||
} | ||
|
||
def byteCountBinary(bytes: Long): String = { | ||
format(bytes, false) | ||
} | ||
|
||
private[util] val (df0, df1, df2) = { | ||
def withDownRound(df: DecimalFormat) = { | ||
df.setRoundingMode(RoundingMode.DOWN) | ||
df | ||
} | ||
( | ||
withDownRound(new DecimalFormat("0")), | ||
withDownRound(new DecimalFormat("0.#")), | ||
withDownRound(new DecimalFormat("0.##")), | ||
) | ||
} | ||
|
||
private def format(bytes: Long, si: Boolean) = { | ||
val absBytes = if (bytes == Long.MinValue) Long.MaxValue else Math.abs(bytes) | ||
val (baseValue, unitStrings) = | ||
if (si) | ||
(1000, Vector("k", "M", "G", "T", "P", "E", "Z", "Y")) | ||
else | ||
(1024, Vector("Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi")) | ||
|
||
def getExponent(curBytes: Long, baseValue: Int, curExponent: Int = 0): Int = { | ||
if (curBytes < baseValue) { | ||
curExponent | ||
} else { | ||
val newExponent = 1 + curExponent | ||
getExponent(curBytes / baseValue, baseValue, newExponent) | ||
} | ||
} | ||
|
||
if (absBytes < baseValue) { | ||
s"$bytes B" | ||
} else { | ||
val signum = if (bytes < 0) "-" else "" | ||
val exponent = getExponent(absBytes, baseValue) | ||
val divisor = Math.pow(baseValue.toDouble, exponent.toDouble) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. Seems both simpler and more precise to just return both exponent and divisor from getExponent. (In the recursive step, |
||
val unitString = unitStrings(exponent - 1) | ||
|
||
val res = absBytes / divisor | ||
3 - res.toLong.toString.length match { | ||
case 2 => f"$signum${df2.format(res)} ${unitString}B" | ||
case 1 => f"$signum${df1.format(res)} ${unitString}B" | ||
case _ => f"$signum${df0.format(res)} ${unitString}B" | ||
} | ||
|
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package io.flow.util | ||
|
||
import scala.concurrent.duration.FiniteDuration | ||
|
||
object DurationFormatter { | ||
import ByteFormatter.{df0, df1, df2} | ||
private val units = Seq((1000L, "ns"), (1000L, "us"), (1000L, "ms"), (60L, "sec"), (60L, "min"), (24L, "h")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not |
||
|
||
def format(duration: FiniteDuration): String = { | ||
val nanos = duration.toNanos.toDouble | ||
val (t, u) = units.foldLeft((nanos, None: Option[String])) { case ((time, result), (base, unit)) => | ||
result.fold { | ||
if (time < base) { | ||
(time, Some(unit)) | ||
} else { | ||
(time / base, None) | ||
} | ||
}(_ => (time, result)) | ||
} | ||
val unit = u.getOrElse("days") | ||
3 - t.toLong.toString.length match { | ||
case 2 => f"${df2.format(t)} $unit" | ||
case 1 => f"${df1.format(t)} $unit" | ||
case _ => f"${df0.format(t)} $unit" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package io.flow.util | ||
|
||
import org.scalatest.matchers.must.Matchers | ||
import org.scalatest.wordspec.AnyWordSpec | ||
|
||
class ByteFormatterSpec extends AnyWordSpec with Matchers { | ||
"SI byte count" should { | ||
"byte values" in { | ||
ByteFormatter.byteCountSI(0L) mustBe "0 B" | ||
ByteFormatter.byteCountSI(1L) mustBe "1 B" | ||
ByteFormatter.byteCountSI(10L) mustBe "10 B" | ||
ByteFormatter.byteCountSI(100L) mustBe "100 B" | ||
ByteFormatter.byteCountSI(999L) mustBe "999 B" | ||
} | ||
|
||
"KB values" in { | ||
ByteFormatter.byteCountSI(1000L) mustBe "1 kB" | ||
ByteFormatter.byteCountSI(1009L) mustBe "1 kB" | ||
ByteFormatter.byteCountSI(1010L) mustBe "1.01 kB" | ||
ByteFormatter.byteCountSI(9999L) mustBe "9.99 kB" | ||
ByteFormatter.byteCountSI(10000L) mustBe "10 kB" | ||
ByteFormatter.byteCountSI(10099L) mustBe "10 kB" | ||
ByteFormatter.byteCountSI(10100L) mustBe "10.1 kB" | ||
ByteFormatter.byteCountSI(99999L) mustBe "99.9 kB" | ||
ByteFormatter.byteCountSI(100000L) mustBe "100 kB" | ||
ByteFormatter.byteCountSI(100999L) mustBe "100 kB" | ||
ByteFormatter.byteCountSI(101000L) mustBe "101 kB" | ||
ByteFormatter.byteCountSI(999999L) mustBe "999 kB" | ||
} | ||
|
||
"MB values" in { | ||
ByteFormatter.byteCountSI(1000000L) mustBe "1 MB" | ||
ByteFormatter.byteCountSI(999999999L) mustBe "999 MB" | ||
} | ||
|
||
"GB values" in { | ||
ByteFormatter.byteCountSI(1000000000L) mustBe "1 GB" | ||
ByteFormatter.byteCountSI(999999999999L) mustBe "999 GB" | ||
} | ||
|
||
"TB values" in { | ||
ByteFormatter.byteCountSI(1000000000000L) mustBe "1 TB" | ||
ByteFormatter.byteCountSI(999999999999999L) mustBe "999 TB" | ||
} | ||
} | ||
|
||
"Binary byte count" should { | ||
"byte values" in { | ||
ByteFormatter.byteCountBinary(0L) mustBe "0 B" | ||
ByteFormatter.byteCountBinary(1L) mustBe "1 B" | ||
ByteFormatter.byteCountBinary(10L) mustBe "10 B" | ||
ByteFormatter.byteCountBinary(100L) mustBe "100 B" | ||
ByteFormatter.byteCountBinary(999L) mustBe "999 B" | ||
ByteFormatter.byteCountBinary(1023L) mustBe "1023 B" | ||
} | ||
|
||
"KiB values" in { | ||
ByteFormatter.byteCountBinary(1024L) mustBe "1 KiB" | ||
ByteFormatter.byteCountBinary(1034L) mustBe "1 KiB" | ||
ByteFormatter.byteCountBinary(1035L) mustBe "1.01 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 10 - 1) mustBe "9.99 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 10) mustBe "10 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 11 - 1) mustBe "10.9 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 11) mustBe "11 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 100 - 1) mustBe "99.9 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 100) mustBe "100 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 128 - 1) mustBe "127 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 128) mustBe "128 KiB" | ||
ByteFormatter.byteCountBinary(1024L * 1024 - 1) mustBe "1023 KiB" | ||
} | ||
|
||
"MiB values" in { | ||
ByteFormatter.byteCountBinary(1024L * 1024) mustBe "1 MiB" | ||
ByteFormatter.byteCountBinary(1024L * 1024 * 1024 - 1) mustBe "1023 MiB" | ||
} | ||
|
||
"GiB values" in { | ||
ByteFormatter.byteCountBinary(1024L * 1024 * 1024) mustBe "1 GiB" | ||
ByteFormatter.byteCountBinary(1024L * 1024 * 1024 * 1024 - 1) mustBe "1023 GiB" | ||
} | ||
|
||
"TiB values" in { | ||
ByteFormatter.byteCountBinary(1024L * 1024 * 1024 * 1024) mustBe "1 TiB" | ||
ByteFormatter.byteCountBinary(1024L * 1024 * 1024 * 1024 * 1024 - 1) mustBe "1023 TiB" | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about larger values; can the unit lookup fail? |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package io.flow.util | ||
|
||
import org.scalatest.matchers.must.Matchers | ||
import org.scalatest.wordspec.AnyWordSpec | ||
import scala.concurrent.duration._ | ||
|
||
class DurationFormatterSpec extends AnyWordSpec with Matchers { | ||
"nanosecond values" in { | ||
DurationFormatter.format(0.nanos) mustBe "0 ns" | ||
DurationFormatter.format(1.nanos) mustBe "1 ns" | ||
DurationFormatter.format(9.nanos) mustBe "9 ns" | ||
DurationFormatter.format(10.nanos) mustBe "10 ns" | ||
DurationFormatter.format(99.nanos) mustBe "99 ns" | ||
DurationFormatter.format(100.nanos) mustBe "100 ns" | ||
DurationFormatter.format(999.nanos) mustBe "999 ns" | ||
} | ||
|
||
"microsecond values" in { | ||
DurationFormatter.format(1000.nanos) mustBe "1 us" | ||
DurationFormatter.format(1009.nanos) mustBe "1 us" | ||
DurationFormatter.format(1010.nanos) mustBe "1.01 us" | ||
DurationFormatter.format(1099.nanos) mustBe "1.09 us" | ||
DurationFormatter.format(1100.nanos) mustBe "1.1 us" | ||
DurationFormatter.format(9999.nanos) mustBe "9.99 us" | ||
DurationFormatter.format(10000.nanos) mustBe "10 us" | ||
DurationFormatter.format(10099.nanos) mustBe "10 us" | ||
DurationFormatter.format(10100.nanos) mustBe "10.1 us" | ||
DurationFormatter.format(99999.nanos) mustBe "99.9 us" | ||
DurationFormatter.format(100000.nanos) mustBe "100 us" | ||
DurationFormatter.format(999999.nanos) mustBe "999 us" | ||
} | ||
|
||
"millisecond values" in { | ||
DurationFormatter.format(1.milli) mustBe "1 ms" | ||
DurationFormatter.format(1000000.nanos) mustBe "1 ms" | ||
DurationFormatter.format(999.millis) mustBe "999 ms" | ||
DurationFormatter.format(999999999.nanos) mustBe "999 ms" | ||
} | ||
|
||
"second values" in { | ||
DurationFormatter.format(1.second) mustBe "1 sec" | ||
DurationFormatter.format(1000.millis) mustBe "1 sec" | ||
DurationFormatter.format(1000000000.nanos) mustBe "1 sec" | ||
DurationFormatter.format(59.seconds) mustBe "59 sec" | ||
DurationFormatter.format(59999.millis) mustBe "59.9 sec" | ||
DurationFormatter.format(59999999999L.nanos) mustBe "59.9 sec" | ||
} | ||
|
||
"minute values" in { | ||
DurationFormatter.format(1.minute) mustBe "1 min" | ||
DurationFormatter.format(60.seconds) mustBe "1 min" | ||
DurationFormatter.format(60000000000L.nanos) mustBe "1 min" | ||
DurationFormatter.format(59.minutes) mustBe "59 min" | ||
DurationFormatter.format(3599.seconds) mustBe "59.9 min" | ||
DurationFormatter.format(3599999999999L.nanos) mustBe "59.9 min" | ||
} | ||
|
||
"hour values" in { | ||
DurationFormatter.format(1.hour) mustBe "1 h" | ||
DurationFormatter.format(60.minutes) mustBe "1 h" | ||
DurationFormatter.format(3600000000000L.nanos) mustBe "1 h" | ||
DurationFormatter.format(23.hours) mustBe "23 h" | ||
DurationFormatter.format(1439.minutes) mustBe "23.9 h" | ||
DurationFormatter.format(86399999999999L.nanos) mustBe "23.9 h" | ||
} | ||
|
||
"day values" in { | ||
DurationFormatter.format(1.day) mustBe "1 days" | ||
DurationFormatter.format(24.hours) mustBe "1 days" | ||
DurationFormatter.format(86400000000000L.nanos) mustBe "1 days" | ||
DurationFormatter.format(100000.days) mustBe "100000 days" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how you feel about flag arguments but throwing in an alternative, e.g.,
format(value, multiplier)
where the multiplier has only two cases (SI, Binary) and the list of symbols is derived from it with pattern matching.