meshBlog
JSR-363 Units of Measurement API in Practice – Binary Prefixes
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.
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)
}
}
}
}
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)
}