Skip to content

Instantly share code, notes, and snippets.

@agolovenko
Created June 15, 2021 17:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save agolovenko/d9b72b3cb91d00ba0ccf56761385968a to your computer and use it in GitHub Desktop.
Save agolovenko/d9b72b3cb91d00ba0ccf56761385968a to your computer and use it in GitHub Desktop.
InformationSize: scala implementation for human-readable file sizes
final case class InformationSize(size: Double, unit: InformationUnit) extends Ordered[InformationSize] {
import InformationSize.{safeAdd, safeDivide, safeMultiply}
import InformationUnit._
if (size < 0d) throw new IllegalArgumentException(s"Unsupported negative size: $size")
def toBits: Double = to(Bit)
def toKiloBits: Double = to(KiloBit)
def toMegaBits: Double = to(MegaBit)
def toBytes: Double = to(Byte)
def toMegaBytes: Double = to(MegaByte)
def toGigaBytes: Double = to(GigaByte)
def toTeraBytes: Double = to(TeraByte)
def toPetaBytes: Double = to(PetaByte)
def to(toUnit: InformationUnit): Double = safeMultiply(size / toUnit.bits, unit.bits.toDouble)
def +(other: InformationSize): InformationSize = copy(size = safeAdd(this.size, other.to(this.unit)))
def -(other: InformationSize): InformationSize = copy(size = this.size - other.to(this.unit))
def *(by: Double): InformationSize = copy(size = safeMultiply(this.size, by))
def /(by: Double): InformationSize = copy(size = safeDivide(this.size, by))
override def compare(other: InformationSize): Int = this.size.compareTo(other.to(this.unit))
override def equals(other: Any): Boolean = other match {
case otherSize: InformationSize => this.size == otherSize.to(this.unit)
case _ => false
}
override def hashCode(): Int = toBits.hashCode()
override def toString: String =
if (Math.floor(size) != size) f"$size%1.2f ${unit.label}s"
else f"$size%1.0f ${unit.label}${if (size == 1d) "" else "s"}"
}
object InformationSize {
private val regex = """(\d+(\.\d+)?)\s*(\w+)""".r
private val lookup = (for {
unit <- InformationUnit.allUnits
label <- unit.labels
} yield label -> unit).toMap
def apply(s: String): InformationSize = s.trim match {
case regex(size, _, unit) =>
InformationSize(size.toDouble, lookup.getOrElse(unit.toLowerCase, throw new IllegalArgumentException(s"Unsupported unit: '$unit'")))
case _ => throw new IllegalArgumentException(s"Failed to parse from '${s.trim}'")
}
trait ImplicitConversions extends Any {
import InformationUnit._
protected def size: Double
def bits = InformationSize(size, Bit)
def kiloBits = InformationSize(size, KiloBit)
def megaBits = InformationSize(size, MegaBit)
def bytes = InformationSize(size, Byte)
def kb = InformationSize(size, KiloByte)
def kiloBytes = kb
def mb = InformationSize(size, MegaByte)
def megaBytes = mb
def gb = InformationSize(size, GigaByte)
def gigaBytes = gb
def tb = InformationSize(size, TeraByte)
def teraBytes = tb
def pb = InformationSize(size, PetaByte)
def petaBytes = pb
}
implicit final class InformationSizeInt(private val i: Int) extends AnyVal with ImplicitConversions {
override protected def size: Double = i.toDouble
}
implicit final class InformationSizeLong(private val l: Long) extends AnyVal with ImplicitConversions {
override protected def size: Double = l.toDouble
}
implicit final class InformationSizeDouble(private val d: Double) extends AnyVal with ImplicitConversions {
override protected def size: Double = d
}
private def safeAdd(a: Double, b: Double) = {
if (a > Double.MaxValue - b) throw new IllegalArgumentException(s"Math operation '$a + $b' causes number overflow")
a + b
}
private def safeMultiply(a: Double, b: Double) = {
if (b < 0) throw new IllegalArgumentException(s"Unsupported negative multiplication factor: $b")
if (b > 1d && a > Double.MaxValue / b) throw new IllegalArgumentException(s"Math operation '$a * $b' causes number overflow")
a * b
}
private def safeDivide(a: Double, b: Double) = {
if (b < 0) throw new IllegalArgumentException(s"Unsupported negative division factor: $b")
if (b < 1d && a > Double.MaxValue * b) throw new IllegalArgumentException(s"Math operation '$a / $b' causes number overflow")
a / b
}
}
trait InformationUnit extends Serializable {
def bits: Long
def labels: Seq[String]
def label: String = labels.head
}
object InformationUnit {
val allUnits: Seq[InformationUnit] = Seq(Bit, KiloBit, MegaBit, Byte, KiloByte, MegaByte, GigaByte, TeraByte, PetaByte)
case object Bit extends InformationUnit {
override def bits: Long = 1L
override def labels: Seq[String] = Seq("bit", "bits")
}
case object KiloBit extends InformationUnit {
override def bits: Long = 1L << 10
override def labels: Seq[String] = Seq("kilobit", "kilobits", "kbit", "kbits")
}
case object MegaBit extends InformationUnit {
override def bits: Long = 1L << 20
override def labels: Seq[String] = Seq("megabit", "megabits", "mbit", "mbits")
}
case object Byte extends InformationUnit {
override def bits: Long = 8L
override def labels: Seq[String] = Seq("byte", "bytes", "b")
}
case object KiloByte extends InformationUnit {
override def bits: Long = 1L << 13
override def labels: Seq[String] = Seq("kilobyte", "kilobytes", "kb", "k")
}
case object MegaByte extends InformationUnit {
override def bits: Long = 1L << 23
override def labels: Seq[String] = Seq("megabyte", "megabytes", "mb", "m")
}
case object GigaByte extends InformationUnit {
override def bits: Long = 1L << 33
override def labels: Seq[String] = Seq("gigabyte", "gigabytes", "gb", "g")
}
case object TeraByte extends InformationUnit {
override def bits: Long = 1L << 43
override def labels: Seq[String] = Seq("terabyte", "terabytes", "tb", "t")
}
case object PetaByte extends InformationUnit {
override def bits: Long = 1L << 53
override def labels: Seq[String] = Seq("petabyte", "petabytes", "pb", "p")
}
}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import InformationSize.{InformationSizeDouble, InformationSizeInt}
class InformationSizeSpec extends AnyWordSpec with Matchers {
"parses strings" in {
InformationSize("16 bits") shouldBe 16.bits
InformationSize("1 kbit ") shouldBe 1.kiloBits
InformationSize("2megabits") shouldBe 2.megaBits
InformationSize("1.23 bytes") shouldBe 1.23d.bytes
InformationSize("1 KB") shouldBe 1.kb
InformationSize("32M") shouldBe 32.megaBytes
InformationSize("32gb") shouldBe 32.gigaBytes
InformationSize("32TB") shouldBe 32.teraBytes
InformationSize("1 petaByte") shouldBe 1.petaBytes
}
"throws on invalid strings" in {
an[IllegalArgumentException] shouldBe thrownBy(InformationSize("1. bits"))
an[IllegalArgumentException] shouldBe thrownBy(InformationSize("3kib"))
}
"implements equality" in {
10.kiloBytes shouldBe (8 * 10).kiloBits
1024.megaBytes shouldBe (1d / 1024).teraBytes
2.teraBytes shouldBe (1d / 512).petaBytes
}
"implements comparison" in {
1000.kiloBytes should be < 1.megaBytes
1000.bits should be > 0.1d.kiloBytes
}
"implements math" in {
2.bytes - 5.bits shouldBe 11.bits
1.megaBytes + 20.kb shouldBe 1044.kb
3.teraBytes * 6d shouldBe 18.tb
6.bytes / 4d shouldBe 12.bits
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment