Added another function that converts bytes/ms to human-readable values without loss of resolution.

This commit is contained in:
Dana Van Aken 2019-10-08 06:09:45 -04:00
parent 3d70990ff1
commit e8f28ebef0
6 changed files with 337 additions and 109 deletions

View File

@ -5,8 +5,11 @@
#
import string
from collections import OrderedDict
import numpy as np
from django.test import TestCase
from website.utils import JSONUtil, MediaUtil, DataUtil, ConversionUtil, LabelUtil, TaskUtil
from website.types import LabelStyleType, VarType
from website.models import Result, DBMSCatalog
@ -250,40 +253,246 @@ class DataUtilTest(TestCase):
class ConversionUtilTest(TestCase):
def test_get_raw_size(self):
# Bytes - In Bytes
byte_test_convert = ['1PB', '2TB', '3GB', '4MB', '5kB', '6B']
byte_ans = [1024**5, 2 * 1024**4, 3 * 1024**3, 4 * 1024**2, 5 * 1024**1, 6]
for i, byte_test in enumerate(byte_test_convert):
byte_conversion = ConversionUtil.get_raw_size(
byte_test, system=ConversionUtil.DEFAULT_BYTES_SYSTEM)
self.assertEqual(byte_conversion, byte_ans[i])
# Time - In Milliseconds
day_test_convert = ['1000ms', '1s', '10min', '20h', '1d']
day_ans = [1000, 1000, 600000, 72000000, 86400000]
for i, day_test in enumerate(day_test_convert):
day_conversion = ConversionUtil.get_raw_size(
day_test, system=ConversionUtil.DEFAULT_TIME_SYSTEM)
self.assertEqual(day_conversion, day_ans[i])
def setUp(self):
self.bytes_map = OrderedDict(
[(suffix, factor) for factor, suffix in ConversionUtil.DEFAULT_BYTES_SYSTEM])
self.ms_map = OrderedDict(
[(suffix, factor) for factor, suffix in ConversionUtil.DEFAULT_TIME_SYSTEM])
self.from_hr_bytes_simple = ['1PB', '2TB', '3GB', '4MB', '1024MB', '5kB', '6B']
self.as_bytes_simple = [1024**5, 2 * 1024**4, 3 * 1024**3, 4 * 1024**2, 1024**3,
5 * 1024, 6]
self.bytes_to_hr_simple = ['1PB', '2TB', '3GB', '4MB', '1GB', '5kB', '6B']
self.assertListEqual(
[len(l) for l in (self.from_hr_bytes_simple, self.as_bytes_simple,
self.bytes_to_hr_simple)], [len(self.from_hr_bytes_simple)] * 3)
self.from_hr_times_simple = ['500ms', '1000ms', '1s', '55s', '10min', '20h', '1d']
self.as_ms_simple = [500, 1000, 1000, 55000, 600000, 72000000, 86400000]
self.ms_to_hr_simple = ['500ms', '1s', '1s', '55s', '10min', '20h', '1d']
self.assertListEqual(
[len(l) for l in (self.from_hr_times_simple, self.as_ms_simple,
self.ms_to_hr_simple)], [len(self.from_hr_times_simple)] * 3)
extra_bytes = [3 * factor for factor in self.bytes_map.values()]
neb = len(extra_bytes)
self.test_bytes_lengths = []
self.from_hr_bytes = [
'1PB', '43PB', '1023PB', '1024PB', '1025PB',
'1TB', '43TB', '1023TB', '1024TB', '1025TB',
'1GB', '43GB', '1023GB', '1024GB', '1025GB',
'1MB', '43MB', '1023MB', '1024MB', '1025MB',
'1kB', '43kB', '1023kB', '1024kB', '1025kB',
'1B', '43B', '1023B', '1024B', '1025B',
'46170898432MB', '45088768MB', '44032MB',
'44032kB', '44032B', '43kB',
] + ['43{}'.format(suffix) for suffix in list(self.bytes_map.keys())[1:]] + \
['{}B'.format(sum(extra_bytes[i:])) for i in range(neb)]
self.test_bytes_lengths.append(len(self.from_hr_bytes))
self.as_bytes = [
1024**5, 43 * 1024**5, 1023 * 1024**5, 1024 * 1024**5, 1025 * 1024**5,
1024**4, 43 * 1024**4, 1023 * 1024**4, 1024 * 1024**4, 1025 * 1024**4,
1024**3, 43 * 1024**3, 1023 * 1024**3, 1024 * 1024**3, 1025 * 1024**3,
1024**2, 43 * 1024**2, 1023 * 1024**2, 1024 * 1024**2, 1025 * 1024**2,
1024**1, 43 * 1024**1, 1023 * 1024**1, 1024 * 1024**1, 1025 * 1024**1,
1024**0, 43 * 1024**0, 1023 * 1024**0, 1024 * 1024**0, 1025 * 1024**0,
46170898432 * 1024**2, 45088768 * 1024**2, 44032 * 1024**2,
44032 * 1024, 44032, 43 * 1024,
] + [43 * 1024**i for i in range(len(self.bytes_map) - 1)[::-1]] + \
[sum(extra_bytes[i:]) for i in range(neb)]
self.test_bytes_lengths.append(len(self.as_bytes))
self.bytes_to_hr = [
'1PB', '43PB', '1023PB', '1024PB', '1025PB',
'1TB', '43TB', '1023TB', '1PB', '1PB',
'1GB', '43GB', '1023GB', '1TB', '1TB',
'1MB', '43MB', '1023MB', '1GB', '1GB',
'1kB', '43kB', '1023kB', '1MB', '1MB',
'1B', '43B', '1023B', '1kB', '1kB',
'43PB', '43TB', '43GB',
'43MB', '43kB', '43kB',
] + ['43{}'.format(suffix) for suffix in list(self.bytes_map.keys())[1:]] + \
['3{}'.format(suffix) for suffix in self.bytes_map.keys()]
self.test_bytes_lengths.append(len(self.bytes_to_hr))
self.bytes_to_hr2 = [
'1PB', '43PB', '1023PB', '1024PB', '1025PB',
'1TB', '43TB', '1023TB', '1PB', '1025TB',
'1GB', '43GB', '1023GB', '1TB', '1025GB',
'1MB', '43MB', '1023MB', '1GB', '1025MB',
'1kB', '43kB', '1023kB', '1MB', '1025kB',
'1B', '43B', '1023B', '1kB', '1025B',
'43PB', '43TB', '43GB',
'43MB', '43kB', '43kB',
] + ['43{}'.format(suffix) for suffix in list(self.bytes_map.keys())[1:]] + \
['{}kB'.format(sum(extra_bytes[i:]) // 1024) for i in range(neb - 1)] + \
['{}B'.format(extra_bytes[-1])]
self.test_bytes_lengths.append(len(self.bytes_to_hr2))
self.min_bytes_suffixes = (25 * ['kB']) + (11 * ['B']) + \
list(self.bytes_map.keys())[:-1] + \
((neb - 1) * ['kB']) + ['B']
self.test_bytes_lengths.append(len(self.min_bytes_suffixes))
self.assertListEqual(self.test_bytes_lengths,
[self.test_bytes_lengths[0]] * len(self.test_bytes_lengths))
self.test_ms_lengths = []
extra_ms = [3 * factor for factor in self.ms_map.values()]
nem = len(extra_ms)
self.from_hr_times = [
'1d', '5d', '6d', '7d', '8d',
'1h', '5h', '23h', '24h', '25h',
'1min', '5min', '59min', '60min', '61min',
'1s', '5s', '59s', '60s', '61s',
'1ms', '5ms', '999ms', '1000ms', '1001ms',
'7200min', '300min', '300s', '5000ms', '5s',
] + ['5{}'.format(suffix) for suffix in list(self.ms_map.keys())[1:]] + \
['{}ms'.format(sum(extra_ms[i:])) for i in range(nem)]
self.test_ms_lengths.append(len(self.from_hr_times))
self.as_ms = [v * 86400000 for v in (1, 5, 6, 7, 8)] + \
[v * 3600000 for v in (1, 5, 23, 24, 25)] + \
[v * 60000 for v in (1, 5, 59, 60, 61)] + \
[v * 1000 for v in (1, 5, 59, 60, 61)] + \
[v * 1 for v in (1, 5, 999, 1000, 1001)] + \
[432000000, 18000000, 300000, 5000, 5000] + \
[5 * v for v in (3600000, 60000, 1000, 1)] + \
[sum(extra_ms[i:]) for i in range(nem)]
self.test_ms_lengths.append(len(self.as_ms))
self.ms_to_hr = [
'1d', '5d', '6d', '7d', '8d',
'1h', '5h', '23h', '1d', '1d',
'1min', '5min', '59min', '1h', '1h',
'1s', '5s', '59s', '1min', '1min',
'1ms', '5ms', '999ms', '1s', '1s',
'5d', '5h', '5min', '5s', '5s',
] + ['5{}'.format(suffix) for suffix in list(self.ms_map.keys())[1:]] + \
['3{}'.format(suffix) for suffix in self.ms_map.keys()]
self.test_ms_lengths.append(len(self.ms_to_hr))
self.ms_to_hr2 = [
'1d', '5d', '6d', '7d', '8d',
'1h', '5h', '23h', '1d', '25h',
'1min', '5min', '59min', '1h', '61min',
'1s', '5s', '59s', '1min', '61s',
'1ms', '5ms', '999ms', '1s', '1001ms',
'5d', '5h', '5min', '5s', '5s',
] + ['5{}'.format(suffix) for suffix in list(self.ms_map.keys())[1:]] + \
['{}s'.format(sum(extra_ms[i:]) // 1000) for i in range(nem - 1)] + \
['{}ms'.format(extra_ms[-1])]
self.test_ms_lengths.append(len(self.ms_to_hr2))
self.min_time_suffixes = (20 * ['s']) + (10 * ['ms']) + list(self.ms_map.keys())[:-1] + \
((nem - 1) * ['s']) + ['ms']
self.test_ms_lengths.append(len(self.min_time_suffixes))
self.assertListEqual(self.test_ms_lengths,
[self.test_ms_lengths[0]] * len(self.test_ms_lengths))
def test_default_system(self):
expected_byte_units = ('B', 'kB', 'MB', 'GB', 'TB', 'PB')
expected_byte_values = (1, 1024, 1024**2, 1024**3, 1024**4, 1024**5)
self.assertEqual(set(self.bytes_map.keys()), set(expected_byte_units))
for unit, exp_val in zip(expected_byte_units, expected_byte_values):
self.assertEqual(self.bytes_map[unit], exp_val)
expected_time_units = ('ms', 's', 'min', 'h', 'd')
expected_time_values = (1, 1000, 60000, 3600000, 86400000)
self.assertEqual(set(self.ms_map.keys()), set(expected_time_units))
for unit, exp_val in zip(expected_time_units, expected_time_values):
self.assertEqual(self.ms_map[unit], exp_val)
def test_get_raw_size_simple(self):
# Bytes
for hr_value, exp_value in zip(self.from_hr_bytes_simple, self.as_bytes_simple):
value = ConversionUtil.get_raw_size(
hr_value, system=ConversionUtil.DEFAULT_BYTES_SYSTEM)
self.assertEqual(value, exp_value)
# Time
for hr_value, exp_value in zip(self.from_hr_times_simple, self.as_ms_simple):
value = ConversionUtil.get_raw_size(
hr_value, system=ConversionUtil.DEFAULT_TIME_SYSTEM)
self.assertEqual(value, exp_value)
def test_get_raw_size(self):
# Bytes
for hr_value, exp_value in zip(self.from_hr_bytes, self.as_bytes):
byte_conversion = ConversionUtil.get_raw_size(
hr_value, system=ConversionUtil.DEFAULT_BYTES_SYSTEM)
self.assertEqual(byte_conversion, exp_value)
# Time
for hr_value, exp_value in zip(self.from_hr_times, self.as_ms):
time_conversion = ConversionUtil.get_raw_size(
hr_value, system=ConversionUtil.DEFAULT_TIME_SYSTEM)
self.assertEqual(time_conversion, exp_value)
def test_get_human_readable_simple(self):
# Bytes
for raw_value, exp_value in zip(self.as_bytes_simple, self.bytes_to_hr_simple):
value = ConversionUtil.get_human_readable(
raw_value, system=ConversionUtil.DEFAULT_BYTES_SYSTEM)
self.assertEqual(value, exp_value)
value2 = ConversionUtil.get_human_readable2(
raw_value, system=ConversionUtil.DEFAULT_BYTES_SYSTEM,
min_suffix='B')
self.assertEqual(value2, exp_value)
value = ConversionUtil.get_human_readable2(
44, system=ConversionUtil.DEFAULT_BYTES_SYSTEM,
min_suffix='kB')
self.assertEqual(value, '44B')
# Time
for raw_value, exp_value in zip(self.as_ms_simple, self.ms_to_hr_simple):
value = ConversionUtil.get_human_readable(
raw_value, system=ConversionUtil.DEFAULT_TIME_SYSTEM)
self.assertEqual(value, exp_value)
value2 = ConversionUtil.get_human_readable2(
raw_value, system=ConversionUtil.DEFAULT_TIME_SYSTEM,
min_suffix='ms')
self.assertEqual(value2, exp_value)
value = ConversionUtil.get_human_readable2(
44, system=ConversionUtil.DEFAULT_TIME_SYSTEM,
min_suffix='s')
self.assertEqual(value, '44ms')
def test_get_human_readable(self):
# Bytes
byte_test_convert = [1024**5, 2 * 1024**4, 3 * 1024**3,
4 * 1024**2, 5 * 1024**1, 6]
byte_ans = ['1PB', '2TB', '3GB', '4MB', '5kB', '6B']
for i, byte_test in enumerate(byte_test_convert):
byte_readable = ConversionUtil.get_human_readable(
byte_test, system=ConversionUtil.DEFAULT_BYTES_SYSTEM)
self.assertEqual(byte_readable, byte_ans[i])
for i, raw_bytes in enumerate(self.as_bytes):
exp_hr = self.bytes_to_hr[i]
exp_hr2 = self.bytes_to_hr2[i]
min_suffix = self.min_bytes_suffixes[i]
hr_value = ConversionUtil.get_human_readable(
raw_bytes, system=ConversionUtil.DEFAULT_BYTES_SYSTEM)
hr_value2 = ConversionUtil.get_human_readable2(
raw_bytes, system=ConversionUtil.DEFAULT_BYTES_SYSTEM,
min_suffix=min_suffix)
self.assertEqual(hr_value, exp_hr)
self.assertEqual(hr_value2, exp_hr2)
# Time
day_test_convert = [500, 1000, 55000, 600000, 72000000, 86400000]
day_ans = ['500ms', '1s', '55s', '10min', '20h', '1d']
for i, day_test in enumerate(day_test_convert):
day_readable = ConversionUtil.get_human_readable(
day_test, system=ConversionUtil.DEFAULT_TIME_SYSTEM)
self.assertEqual(day_readable, day_ans[i])
for i, raw_time in enumerate(self.as_ms):
exp_hr = self.ms_to_hr[i]
exp_hr2 = self.ms_to_hr2[i]
min_suffix = self.min_time_suffixes[i]
hr_value = ConversionUtil.get_human_readable(
raw_time, system=ConversionUtil.DEFAULT_TIME_SYSTEM)
hr_value2 = ConversionUtil.get_human_readable2(
raw_time, system=ConversionUtil.DEFAULT_TIME_SYSTEM,
min_suffix=min_suffix)
self.assertEqual(hr_value, exp_hr)
self.assertEqual(hr_value2, exp_hr2)
class LabelUtilTest(TestCase):
@ -314,7 +523,7 @@ class LabelUtilTest(TestCase):
"Random Word"]
for i, key in enumerate(test_keys):
if (key == "???"): # DBms -> DBMS or DBms?
if key == "???": # DBms -> DBMS or DBms?
continue
self.assertEqual(res_capfirst_label_map[key], cap_ans[i])

View File

@ -5,8 +5,9 @@
#
from collections import OrderedDict
from website.models import KnobCatalog, MetricCatalog
from website.models import KnobCatalog, KnobUnitType, MetricCatalog
from website.types import BooleanType, MetricType, VarType
from website.utils import ConversionUtil
# pylint: disable=no-self-use
@ -31,6 +32,11 @@ class BaseParser:
self.true_value = 'on'
self.false_value = 'off'
self.bytes_system = ConversionUtil.DEFAULT_BYTES_SYSTEM
self.time_system = ConversionUtil.DEFAULT_TIME_SYSTEM
self.min_bytes_unit = 'kB'
self.min_time_unit = 'ms'
@property
def transactions_counter(self):
raise NotImplementedError()
@ -79,11 +85,25 @@ class BaseParser:
def convert_integer(self, int_value, metadata):
try:
res = int(int_value)
try:
converted = int(int_value)
except ValueError:
res = int(float(int_value))
converted = int(float(int_value))
return res
except ValueError:
if metadata.unit == KnobUnitType.BYTES:
converted = ConversionUtil.get_raw_size(
int_value, system=self.bytes_system)
elif metadata.unit == KnobUnitType.MILLISECONDS:
converted = ConversionUtil.get_raw_size(
int_value, system=self.time_system)
else:
raise Exception(
'Unknown unit type: {}'.format(metadata.unit))
if converted is None:
raise Exception('Invalid integer format for {}: {}'.format(
metadata.name, int_value))
return converted
def convert_real(self, real_value, metadata):
return float(real_value)
@ -307,7 +327,11 @@ class BaseParser:
adj_val = end_val - start_val
else: # MetricType.STATISTICS or MetricType.INFO
adj_val = end_val
assert adj_val >= 0, '{} wrong metric type '.format(met_name)
assert adj_val >= 0, \
'{} wrong metric type: {} (start={}, end={}, diff={})'.format(
met_name, MetricType.name(met_info.metric_type), start_val,
end_val, end_val - start_val)
adjusted_metrics[met_name] = adj_val
else:
# This metric is either a bool, enum, string, or timestamp
@ -333,8 +357,24 @@ class BaseParser:
enumvals = metadata.enumvals.split(',')
return enumvals[int(round(enum_value))]
# def format_integer(self, int_value, metadata):
# return int(round(int_value))
def format_integer(self, int_value, metadata):
return int(round(int_value))
int_value = int(round(int_value))
if metadata.unit != KnobUnitType.OTHER and int_value > 0:
if metadata.unit == KnobUnitType.BYTES:
int_value = ConversionUtil.get_human_readable2(
int_value, self.bytes_system, 'kB')
elif metadata.unit == KnobUnitType.MILLISECONDS:
int_value = ConversionUtil.get_human_readable2(
int_value, self.time_system, 'ms')
else:
raise Exception(
'Invalid unit type for {}: {}'.format(
metadata.name, metadata.unit))
return int_value
def format_real(self, real_value, metadata):
return round(float(real_value), 3)

View File

@ -8,8 +8,7 @@ import re
from collections import OrderedDict
from .base import BaseParser
from website.types import KnobUnitType, MetricType, VarType
from website.utils import ConversionUtil
from website.types import MetricType, VarType
class MyRocksParser(BaseParser):
@ -22,40 +21,6 @@ class MyRocksParser(BaseParser):
def latency_timer(self):
raise NotImplementedError()
def convert_integer(self, int_value, metadata):
converted = None
try:
converted = super().convert_integer(
int_value, metadata)
except ValueError:
if metadata.unit == KnobUnitType.BYTES:
converted = ConversionUtil.get_raw_size(
int_value, system=ConversionUtil.DEFAULT_BYTES_SYSTEM)
elif metadata.unit == KnobUnitType.MILLISECONDS:
converted = ConversionUtil.get_raw_size(
int_value, system=ConversionUtil.DEFAULT_TIME_SYSTEM)
else:
raise Exception('Unknown unit type: {}'.format(metadata.unit))
if converted is None:
raise Exception('Invalid integer format for {}: {}'.format(
metadata.name, int_value))
return converted
def format_integer(self, int_value, metadata):
if metadata.unit != KnobUnitType.OTHER and int_value > 0:
if metadata.unit == KnobUnitType.BYTES:
int_value = ConversionUtil.get_human_readable(
int_value, ConversionUtil.DEFAULT_BYTES_SYSTEM)
elif metadata.unit == KnobUnitType.MILLISECONDS:
int_value = ConversionUtil.get_human_readable(
int_value, ConversionUtil.DEFAULT_TIME_SYSTEM)
else:
raise Exception('Invalid unit type for {}: {}'.format(
metadata.name, metadata.unit))
else:
int_value = super().format_integer(int_value, metadata)
return int_value
def parse_version_string(self, version_string):
dbms_version = version_string.split(',')[0]
return re.search(r'\d+\.\d+(?=\.\d+)', dbms_version).group(0)

View File

@ -13,6 +13,13 @@ class OracleParser(BaseParser):
super().__init__(dbms_obj)
self.true_value = 'TRUE'
self.false_value = 'FALSE'
self.bytes_system = (
(1024 ** 4, 'T'),
(1024 ** 3, 'G'),
(1024 ** 2, 'M'),
(1024 ** 1, 'k'),
)
self.min_bytes_unit = 'k'
@property
def transactions_counter(self):

View File

@ -7,7 +7,6 @@
import re
from .base import BaseParser
from website.types import KnobUnitType
from website.utils import ConversionUtil
@ -17,6 +16,8 @@ class PostgresParser(BaseParser):
super().__init__(dbms_obj)
self.valid_true_val = ("on", "true", "yes", 1)
self.valid_false_val = ("off", "false", "no", 0)
self.bytes_system = [(f, s) for f, s in ConversionUtil.DEFAULT_BYTES_SYSTEM
if s in ('TB', 'GB', 'MB', 'kB')]
@property
def transactions_counter(self):
@ -26,42 +27,6 @@ class PostgresParser(BaseParser):
def latency_timer(self):
raise NotImplementedError()
def convert_integer(self, int_value, metadata):
converted = None
try:
converted = super().convert_integer(int_value, metadata)
except ValueError:
if metadata.unit == KnobUnitType.BYTES:
converted = ConversionUtil.get_raw_size(
int_value, system=ConversionUtil.DEFAULT_BYTES_SYSTEM)
elif metadata.unit == KnobUnitType.MILLISECONDS:
converted = ConversionUtil.get_raw_size(
int_value, system=ConversionUtil.DEFAULT_TIME_SYSTEM)
else:
raise Exception(
'Unknown unit type: {}'.format(metadata.unit))
if converted is None:
raise Exception('Invalid integer format for {}: {}'.format(
metadata.name, int_value))
return converted
def format_integer(self, int_value, metadata):
int_value = int(round(int_value))
if metadata.unit != KnobUnitType.OTHER and int_value > 0:
if metadata.unit == KnobUnitType.BYTES:
int_value = ConversionUtil.get_human_readable(
int_value, ConversionUtil.DEFAULT_BYTES_SYSTEM)
elif metadata.unit == KnobUnitType.MILLISECONDS:
int_value = ConversionUtil.get_human_readable(
int_value, ConversionUtil.DEFAULT_TIME_SYSTEM)
else:
raise Exception(
'Invalid unit type for {}: {}'.format(
metadata.name, metadata.unit))
else:
int_value = super().format_integer(int_value, metadata)
return int_value
def parse_version_string(self, version_string):
dbms_version = version_string.split(',')[0]
return re.search(r'\d+\.\d+(?=\.\d+)', dbms_version).group(0)

View File

@ -6,6 +6,7 @@
import datetime
import json
import logging
import math
import os
import string
import tarfile
@ -270,6 +271,47 @@ class ConversionUtil(object):
from hurry.filesize import size
return size(value, system=system)
@staticmethod
def get_human_readable2(value, system, min_suffix):
# Converts the value to larger units only if there is no loss of resolution.
# pylint: disable=line-too-long
# From https://github.com/le0pard/pgtune/blob/master/webpack/components/configurationView/index.jsx#L74
# pylint: enable=line-too-long
min_factor = None
unit = None
mod_system = []
for i, (factor, suffix) in enumerate(system):
if suffix == min_suffix:
if value < factor:
assert i + 1 < len(system), \
('i + 1 >= len(system) (i + 1: {}, len(system): {}, value: {}, '
'min_suffix: {})').format(i + 1, len(system), value, min_suffix)
min_suffix = system[i + 1][1]
LOG.warning('The value is smaller than the min factor: %s < %s%s. '
'Setting min_suffix=%s...', value, factor, suffix, min_suffix)
else:
min_factor = factor
unit = min_suffix
value = math.floor(float(value) / min_factor)
break
mod_system.append((factor, suffix))
if min_factor is None:
raise ValueError('Invalid min suffix for system: suffix={}, system={}'.format(
min_suffix, system))
LOG.info('min_suffix: %s, min_factor: %s, unit: %s, value: %s\nMOD_SYS:\n%s\n\n',
min_suffix, min_factor, unit, value, mod_system)
for factor, suffix in mod_system:
adj_factor = factor / min_factor
if value % adj_factor == 0:
value = math.floor(float(value) / adj_factor)
unit = suffix
break
return '{}{}'.format(int(value), unit)
class LabelUtil(object):