WE ENABLE CLOUD-NATIVE ORGANIZATIONS
meshBlog

Learn more about Cloud, Multi-Cloud and Software Delivery

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:

@Test
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)
}

One response to “JSR-363 Units of Measurement API in Practice – Binary Prefixes”

  1. […] Persisting Quantities with Spring Data 2) Supporting Binary and SI Prefixes 3) Supporting JSON Serialization for REST […]

Leave a Reply to JSR-363 Units of Measurement API in Practice – Blogpost Series Introduction – Meshcloud Cancel reply

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.