Created
June 15, 2021 17:15
-
-
Save agolovenko/d9b72b3cb91d00ba0ccf56761385968a to your computer and use it in GitHub Desktop.
InformationSize: scala implementation for human-readable file sizes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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