When working with units of measurement it's often useful to apply a prefix to capture the order of magnitude. The SI unit system has a standard set of prefixes based on powers of 10, e.g. kilo: 10³ or mega: 10⁶. For quantities of information like bits and bytes, it's however often useful to have prefixes based on powers of 2 like MeBi: 2²⁰. These are also called binary prefixes.

The tec.uom.se library introduced eralier in this series of blog-posts has a class BinaryPrefix, but it only offers methods for unit conversion. Let's build a short helper class and build a nice fluent API in Kotlin so that we can write e.g. 10.mebi.byte for 10 MiB.

enum class BinaryPrefix(
private val symbol: String,
private val converter: RationalConverter
) : SymbolSupplier, UnitConverterSupplier {

YOBI("Yi", RationalConverter(BigInteger.valueOf(1024L).pow(8), BigInteger.ONE)),
ZEBI("Zi", RationalConverter(BigInteger.valueOf(1024L).pow(7), BigInteger.ONE)),
EXBI("Ei", RationalConverter(BigInteger.valueOf(1024L).pow(6), BigInteger.ONE)),
PEBI("Pi", RationalConverter(BigInteger.valueOf(1024L).pow(5), BigInteger.ONE)),
TEBI("Ti", RationalConverter(BigInteger.valueOf(1024L).pow(4), BigInteger.ONE)),
GIBI("Gi", RationalConverter(BigInteger.valueOf(1024L).pow(3), BigInteger.ONE)),
MEBI("Mi", RationalConverter(BigInteger.valueOf(1024L).pow(2), BigInteger.ONE)),
KIBI("Ki", RationalConverter(BigInteger.valueOf(1024L).pow(1), BigInteger.ONE));

override fun getSymbol(): String {
return symbol

override fun getConverter(): UnitConverter {
return converter

val converterFormat: String
get() {
return converter.dividend.toString()

data class BinaryPrefixedNumber(val number: Number, val prefix: BinaryPrefix)

val Number.yobi get() = BinaryPrefixedNumber(this, BinaryPrefix.YOBI)
val Number.zebi get() = BinaryPrefixedNumber(this, BinaryPrefix.ZEBI)
val Number.exbi get() = BinaryPrefixedNumber(this, BinaryPrefix.EXBI)
val Number.pebi get() = BinaryPrefixedNumber(this, BinaryPrefix.PEBI)
val Number.tebi get() = BinaryPrefixedNumber(this, BinaryPrefix.TEBI)
val Number.gibi get() = BinaryPrefixedNumber(this, BinaryPrefix.GIBI)
val Number.mebi get() = BinaryPrefixedNumber(this, BinaryPrefix.MEBI)
val Number.kibi get() = BinaryPrefixedNumber(this, BinaryPrefix.KIBI)

val BinaryPrefixedNumber.byte: ComparableQuantity
get() = number(UCUM.BYTE.transform(prefix.converter))

Another missing feature is support for string serialization of binary-prefixed units. Unfortunately, the UCUMFormatter of the UOM library is not extensible, so we need to add a simple pre/post-processing step to our formatters from the last episode.

object QuantityFormatting { private val unitFormatter = UcumFormatWithBinaryPrefixSupport() private val numberFormatter = NumberFormat.getInstance(Locale.ROOT) private val separator = " " object QuantityToStringConverter : Converter<Quantity<*>, String> { override fun convert(source: Quantity<*>): String { val s = source.toWildcard() val formattedUnit = unitFormatter.format(s.unit) val formattedValue = numberFormatter.format(s.value) return "$formattedValue$separator$formattedUnit" } } object StringToQuantityConverter : Converter<String, Quantity<*>> { override fun convert(source: String): Quantity<*> { val (formattedValue, formattedUnit) = source.split(separator, limit = 2) val parsedUnit: Unit = unitFormatter.parse(formattedUnit).toWildcard() val parsedValue = numberFormatter.parse(formattedValue) return Quantities.getQuantity(parsedValue, parsedUnit) } } class UcumFormatWithBinaryPrefixSupport(val base: UCUMFormat = UCUMFormat.getInstance(UCUMFormat.Variant.CASE_SENSITIVE)) : UnitFormat by base { override fun format(unit: Unit<*>): String { val baseResult = base.format(unit) val split = baseResult.split(".") if (split.size == 1) { return baseResult } val (symbol, converter) = split return when (converter) { BinaryPrefix.YOBI.converterFormat -> "${BinaryPrefix.YOBI.symbol}$symbol" BinaryPrefix.ZEBI.converterFormat -> "${BinaryPrefix.ZEBI.symbol}$symbol" BinaryPrefix.EXBI.converterFormat -> "${BinaryPrefix.EXBI.symbol}$symbol" BinaryPrefix.PEBI.converterFormat -> "${BinaryPrefix.PEBI.symbol}$symbol" BinaryPrefix.TEBI.converterFormat -> "${BinaryPrefix.TEBI.symbol}$symbol" BinaryPrefix.GIBI.converterFormat -> "${BinaryPrefix.GIBI.symbol}$symbol" BinaryPrefix.MEBI.converterFormat -> "${BinaryPrefix.MEBI.symbol}$symbol" BinaryPrefix.KIBI.converterFormat -> "${BinaryPrefix.KIBI.symbol}$symbol" else -> baseResult } } override fun format(unit: Unit<*>, appendable: Appendable): Appendable { return appendable.append(format(unit)) } override fun parse(csq: CharSequence): Unit<*> { // note: all binary prefixes have two chars val prefixLength = 2 val (prefix, originalUnit) = when { csq.length >= prefixLength -> Pair(csq.substring(0, prefixLength), csq.substring(prefixLength)) else -> Pair(null, csq) } return when (prefix) { BinaryPrefix.YOBI.symbol -> base.parse(originalUnit).transform(BinaryPrefix.YOBI.converter) BinaryPrefix.ZEBI.symbol -> base.parse(originalUnit).transform(BinaryPrefix.ZEBI.converter) BinaryPrefix.EXBI.symbol -> base.parse(originalUnit).transform(BinaryPrefix.EXBI.converter) BinaryPrefix.PEBI.symbol -> base.parse(originalUnit).transform(BinaryPrefix.PEBI.converter) BinaryPrefix.TEBI.symbol -> base.parse(originalUnit).transform(BinaryPrefix.TEBI.converter) BinaryPrefix.GIBI.symbol -> base.parse(originalUnit).transform(BinaryPrefix.GIBI.converter) BinaryPrefix.MEBI.symbol -> base.parse(originalUnit).transform(BinaryPrefix.MEBI.converter) BinaryPrefix.KIBI.symbol -> base.parse(originalUnit).transform(BinaryPrefix.KIBI.converter) else -> base.parse(csq) } } } }

With that, we can make the following test pass:

fun serializingBinaryPrefixedUnits(){
val sut = 1.mebi.byte
val serialized = QuantityFormatting.QuantityToStringConverter.convert(sut)

Assert.assertEquals("1 MiBy", serialized)

val deserialized = QuantityFormatting.StringToQuantityConverter.convert(serialized)
Assert.assertEquals(sut, deserialized)

1 Comment

  1. Pingback: JSR-363 Units of Measurement API in Practice – Blogpost Series Introduction – Meshcloud

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.