Source code for modunits

#!/usr/bin/env python3

"""Convert computer-friendly numbers to human-readable numbers with units at various levels of detail."""

# if you want to use me as a python module, please rename me to modunits.py
# if you want to use me as a standalone executable, please rename me to modunits
# if you want to use me as both, please name me whatever and symlink :)

import re
import sys
import typing


[docs]def builtins() -> typing.List[str]: """Legal options for builtin varieties of modular units.""" list_ = ['computer-size', 'computer-bit-seconds', 'computer-byte-hours', 'time'] list_.sort() return list_
[docs]def detail_options() -> typing.List[str]: """Legal options for what level of detail we want.""" list_ = ['list', 'string-list', 'highest', 'two-highest', 'highest-and-fraction', 'all'] list_.sort() return list_
[docs]def units_type() -> typing.List[str]: """Legal options for abbreviation or not.""" list_ = ['unabbreviated', 'abbreviated'] list_.sort() return list_
# This one is very primitive class usage - it's basically just a struct, IE, no methods to speak of. Using tuples and lists was # getting too messy - this is much more descriptive
[docs]class Type(object): """Hold data about whether we should output units abbreviated or unabbreviated, and what to divide by (?).""" # pylint: disable=R0903 # R0903: We don't need a lot of public methods; we're just a container def __init__(self, threshold: typing.Optional[int], unabbreviated: str, abbreviated: str) -> None: """Initialize.""" self.threshold = threshold self.unabbreviated = unabbreviated self.abbreviated = abbreviated
[docs]class Item(object): """Hold one of the modular pieces of our number.""" # pylint: disable=R0903 # R0903: We don't need a lot of public methods def __init__(self, amount: float, type_: Type, after: bool, units: str, fractional_part_length: int) -> None: """Initialize.""" # pylint: disable=R0913 # R0913: We need a number of arguments; I'd make them named-only if this didn't need to run on older python's self.amount = amount self.type_ = type_ self.after = after self.units = units self.fractional_part_length = fractional_part_length # this handles units and before/after, but not things like commas or the detail level. Comma processing and detail level # processing will build on this def __str__(self): """Return a string version.""" str_amount = str(self.amount) if '.' in str_amount and self.fractional_part_length >= 1: fields = str_amount.split('.') if len(fields) == 2: # slow but simple while len(fields[1]) > self.fractional_part_length: fields[1] = fields[1][:-1] while len(fields[1]) < self.fractional_part_length: fields[1] += '0' str_amount = '%s.%s' % (fields[0], fields[1][0:self.fractional_part_length]) if self.units == 'unabbreviated': unit = self.type_.unabbreviated elif self.units == 'abbreviated': unit = self.type_.abbreviated else: sys.stderr.write('%s: Internal error: Illegal value for units\n' % sys.argv[0]) usage(1) if self.after: if self.units == 'abbreviated': str_amount += unit else: str_amount += ' ' + unit else: str_amount = unit + ': ' + str_amount return str_amount
[docs]def modunits( unitsvector: typing.Union[str, typing.List[Type]], number: int, detail: str = 'highest-and-fraction', units: str = "abbreviated", comma: bool = True, reverse: bool = False, after: bool = True, fractional_part_length: int = -1, ): # pylint: disable=R0913 # R0913: We need a number of arguments; I'd make them named-only if this didn't need to run on older python's """ Convert a number with computer-friendly units into something more human-readable. Main entry point for the module. """ iec_fundament = 2 ** 10 si_fundament = 10 ** 3 # detail can be: # list (implies that we will ignore units and comma) # highest (implies that we will ignore comma) # two-highest # highest-and-fraction (default) # all # units can be: # unabbreviated # abbreviated (default) # comma can be: # 0 (default) # 1 # reverse can be: # 0 (default) # 1 # after can be: # 0 (default) # 1 internalvector = unitsvector if isinstance(internalvector, str): if internalvector == 'time': # expects seconds internalnumber = number internalvector = [ Type(60, 'seconds', 's'), Type(60, 'minutes', 'm'), Type(24, 'hours', 'h'), Type(365, 'days', 'da'), Type(100, 'years', 'y'), Type(None, 'centuries', 'c'), ] elif internalvector in ['computer-size-iec']: # expects bytes # http://en.wikipedia.org/wiki/Zettabyte internalnumber = number internalvector = [ Type(iec_fundament, 'bytes', 'B'), Type(iec_fundament, 'kibibytes', 'Ki'), Type(iec_fundament, 'mebibytes', 'Mi'), Type(iec_fundament, 'gibibytes', 'Gi'), Type(iec_fundament, 'tebibytes', 'Ti'), Type(iec_fundament, 'pebibytes', 'Pi'), Type(iec_fundament, 'exbibytes', 'Ei'), Type(iec_fundament, 'zebibytes', 'Zi'), Type(None, 'yobibytes', 'Yi'), ] elif internalvector in ['computer-size', 'computer-size-si']: # expects bytes # http://en.wikipedia.org/wiki/Zettabyte internalnumber = number internalvector = [ Type(si_fundament, 'bytes', 'B'), Type(si_fundament, 'kilobytes', 'k'), Type(si_fundament, 'megabytes', 'M'), Type(si_fundament, 'gigabytes', 'G'), Type(si_fundament, 'terabytes', 'T'), Type(si_fundament, 'petabytes', 'P'), Type(si_fundament, 'exabytes', 'E'), Type(si_fundament, 'zettabytes', 'Z'), Type(None, 'yottabytes', 'Y'), ] elif internalvector in ['computer-bit-seconds', 'computer-bits-per-second-iec']: # expects bits/second internalnumber = number internalvector = [ Type(iec_fundament, 'bits/s', 'b/s'), Type(iec_fundament, 'kibibits/second', 'ki/s'), Type(iec_fundament, 'mebibits/second', 'mi/s'), Type(iec_fundament, 'gibibits/second', 'gi/s'), Type(iec_fundament, 'tebibits/second', 'ti/s'), Type(iec_fundament, 'pebibits/second', 'pi/s'), Type(iec_fundament, 'exbibits/second', 'ei/s'), Type(iec_fundament, 'zebibits/second', 'zi/s'), Type(None, 'yobibits/second', 'yi/s'), ] elif internalvector in ['computer-bits-per-second-si']: # expects bits/second internalnumber = number internalvector = [ Type(si_fundament, 'bits/s', 'b/s'), Type(si_fundament, 'kilobits/second', 'k/s'), Type(si_fundament, 'megabits/second', 'm/s'), Type(si_fundament, 'gigabits/second', 'g/s'), Type(si_fundament, 'terabits/second', 't/s'), Type(si_fundament, 'petabits/second', 'p/s'), Type(si_fundament, 'exabits/second', 'e/s'), Type(si_fundament, 'zettabits/second', 'z/s'), Type(None, 'yottabits/second', 'y/s'), ] elif internalvector in ['computer-byte-hours', 'computer-bytes-per-hour-iec']: # expects bytes/hour internalnumber = number internalvector = [ Type(iec_fundament, 'bytes/hour', 'B/hr'), Type(iec_fundament, 'kibibytes/hour', 'Ki/hr'), Type(iec_fundament, 'mebibytes/hour', 'Mi/hr'), Type(iec_fundament, 'gibibytes/hour', 'Gi/hr'), Type(iec_fundament, 'tebibytes/hour', 'Ti/hr'), Type(iec_fundament, 'pebibytes/hour', 'Pi/hr'), Type(iec_fundament, 'exbibytes/hour', 'Ei/hr'), Type(iec_fundament, 'zebibytes/hour', 'Zi/hr'), Type(None, 'yobibytes/hour', 'Yi/hr'), ] elif internalvector in ['computer-bytes-per-hour-si']: # expects bytes/hour internalnumber = number internalvector = [ Type(si_fundament, 'bytes/hour', 'B/hr'), Type(si_fundament, 'kilobytes/hour', 'k/hr'), Type(si_fundament, 'megabytes/hour', 'M/hr'), Type(si_fundament, 'gigabytes/hour', 'G/hr'), Type(si_fundament, 'terabytes/hour', 'T/hr'), Type(si_fundament, 'petabytes/hour', 'P/hr'), Type(si_fundament, 'exabytes/hour', 'E/hr'), Type(si_fundament, 'zettabytes/hour', 'Z/hr'), Type(None, 'yottabytes/hour', 'Y/hr'), ] else: sys.stderr.write('Sorry, I do not have a predefined units vector called %s\n' % unitsvector) sys.exit(1) assert isinstance(internalvector, list) return _chopit(internalvector, internalnumber, detail, units, comma, reverse, after, fractional_part_length)
def _chopit( list_: typing.List[Type], number: int, detail: str, units: str, comma: int, reverse: int, after: bool, fractional_part_length: int, ): # pylint: disable=R0912,R0913 # R0912: We need a few branches # R0913: We need a number of arguments; I'd make them named-only if this didn't need to run on older python's """Compute our list of numbers.""" temp = number remainder = 0 item_list: typing.List[Item] = [] for element in list_: if temp == 0: break if element.threshold is None: (quotient, remainder) = 0, temp else: (quotient, remainder) = divmod(temp, element.threshold) item_list.append(Item(int(remainder), element, after, units, fractional_part_length)) if element.threshold is None: break # print temp, quotient, remainder, item_list temp = quotient # handle the "0" case a little bit specially here, to avoid special cases below if not item_list: item_list.append(Item(0, list_[0], after, units, fractional_part_length)) # at this point, item_list should be a list of parts of the number, from least significant to most if detail == 'string-list': # pretty simple, and probably the easiest to build on :) Just returns the list we built return [str(item) for item in item_list] # return early - don't fall through elif detail == 'list': # pretty simple, and probably the easiest to build on :) Just returns the list we built return item_list # return early - don't fall through elif detail == 'highest': # chop off all but the most significant term - least significant are in the low-numbered list elements while item_list[1:]: del item_list[0] # and fall through elif detail == 'two-highest': # chop off all but the most significant two terms - least # significant are in the low-numbered list elements while item_list[2:]: del item_list[0] # and fall through elif detail == 'highest-and-fraction': # this one's pretty different from the others, so we handle it # here, entirely, rather than falling through for further # processing. we create a single "item" (of type item_class) that # will hold the whole number + fraction for the most and 2nd most # signficant items. Then we return it immediately. item = item_list[-1] if item_list[1:]: # if we have at least two elements, then use the second from the end for the fraction assert item_list[-2].type_.threshold is not None item.amount += item_list[-2].amount / item_list[-2].type_.threshold return str(item) # return early - don't fall through elif detail == 'all': # don't need to do anything for this one yet :) pass # and fall through else: sys.stderr.write('%s: Internal error: Illegal detail level %s in modunits.py\n' % (sys.argv[0], detail)) # print '3 detail level is',detail # note that even detail == 'highest' works with this code if reverse: # and the units move right alone with the numbers item_list.reverse() if comma: separator = ', ' else: separator = ' ' return separator.join(str(item) for item in item_list)
[docs]def usage(retval: int) -> None: """Give usage message to user.""" sys.stderr.write('Usage: %s [-h] -t type [-d detail] [-u unitstype] [-c] [-r] [-a] [-s] [-n number] [-D digits]\n') sys.stderr.write('-h\t\tgive this help message\n') sys.stderr.write('-t type\t\tSpecify the type of numbers to be reformatted. Required.\n') sys.stderr.write('\t\tCurrently legal values are:\n') for type_ in builtins(): sys.stderr.write('\t\t\t%s\n' % type_) sys.stderr.write('-d detail\tSpecify the type of numbers to be reformatted\n') sys.stderr.write('\t\tCurrently legal values are:\n') for detail in detail_options(): sys.stderr.write('\t\t\t%s\n' % detail) sys.stderr.write('-u unitstype\tSpecify if units should be abbreviated or spelled out in full\n') sys.stderr.write('\t\tCurrently legal values are:\n') for units in units_type(): sys.stderr.write('\t\t\t%s\n' % units) sys.stderr.write('-c\t\tseparate with commas\n') sys.stderr.write('-r\t\toutput in reverse\n') sys.stderr.write('-a\t\tput the units after the numbers, not before. A bit shorter\n') sys.stderr.write('-n number\tSpecify a number to be reformatted. Can be repeated\n') sys.stderr.write('-s\t\tRead numbers from stdin\n') sys.stderr.write('\n') sys.stderr.write('-D\t\tNumber of digits to output after a decimal point (ignored for all but -d highest-and-fraction)\n') sys.stderr.write('\n') sys.stderr.write('You can specify both -s and -n, but if you do, the -n numbers\n') sys.stderr.write('\twill be processed before the numbers read from stdin.\n') sys.exit(retval)
[docs]class Options(object): # pylint: disable=R0902 # R0902: We are primarily a container; we need some instance attributes """Hold, parse and verify command line options.""" def __init__(self) -> None: """Initialize.""" self.from_stdin = 0 self.from_argv = 0 self.numbers: typing.List[float] = [] self.units = '' self.detail = 'highest-and-fraction' self.units = "abbreviated" self.comma = False self.reverse = False self.after = False self.type_ = '' self.fractional_part_length = -1
[docs] def parse_argv(self, argv: typing.List[str]) -> None: # pylint: disable=R0912 # R0912: We need a few branches """Dissect our argv into something useful.""" while argv[1:]: if argv[1] in ['-h', '--help']: usage(0) elif argv[1] == '-t' and argv[2:]: self.type_ = argv[2] del argv[1] elif argv[1] == '-d' and argv[2:]: if argv[2] in detail_options(): self.detail = argv[2] else: sys.stderr.write('%s: Illegal detail spec %s\n' % (argv[0], argv[2])) usage(1) del argv[1] elif argv[1] == '-u' and argv[2:]: if argv[2] in units_type(): self.units = argv[2] else: sys.stderr.write('%s: Illegal units spec %s\n' % (argv[0], argv[2])) usage(1) del argv[1] elif argv[1] == '-c': self.comma = True elif argv[1] == '-r': self.reverse = True elif argv[1] == '-a': self.after = True elif argv[1] == '-s': self.from_stdin = 1 elif argv[1] == '-n' and argv[2:]: self.from_argv = 1 whole_number_string = re.sub(r'\.[0-9]*$', '', argv[2]) if int(whole_number_string) == float(argv[2]): self.numbers.append(int(whole_number_string)) else: self.numbers.append(float(argv[2])) del argv[1] elif argv[1] == '-D' and argv[2:]: self.fractional_part_length = int(argv[2]) del argv[1] else: sys.stderr.write('%s: Illegal option %s\n' % (argv[0], argv[1])) usage(1) del argv[1]
[docs] def check_options(self) -> None: """Verify that the options specified make sense.""" if self.type_ == '': sys.stderr.write('%s: -t is a required option\n' % sys.argv[0]) usage(1) if not self.from_argv and not self.from_stdin: sys.stderr.write('%s: Warning: You probably want to specify either -s -or -n\n' % sys.argv[0])
[docs]def main() -> None: """Convert to human-readable form of units.""" options = Options() options.parse_argv(sys.argv) options.check_options() if options.from_argv: for number in options.numbers: print(modunits( options.type_, int(number), options.detail, options.units, options.comma, options.reverse, options.after, options.fractional_part_length, )) if options.from_stdin: for line in sys.stdin: print(modunits( options.type_, int(line), options.detail, options.units, options.comma, options.reverse, options.after, options.fractional_part_length, )) sys.exit(0)
if __name__ == '__main__': main()