[tor-commits] [stem/master] Utils for human readable time and size metrics

atagar at torproject.org atagar at torproject.org
Sat Oct 20 23:02:10 UTC 2012


commit 70227efa08d77f65b1eea069ae93bd0ba0bf1993
Author: Damian Johnson <atagar at torproject.org>
Date:   Fri Oct 19 09:41:34 2012 -0700

    Utils for human readable time and size metrics
    
    Snagging the utilities from arm's src/util/uiTools.py for making human readable
    labels of time and size metrics. They'll not only be handy for stem's users,
    but also our exception messages and something I want to use in our tutorials.
    
    I've simplified these functions a bit and added unit tests for the pydoc
    examples. More tests are needed, though...
---
 run_tests.py                |    2 +
 stem/util/enum.py           |   24 +----
 stem/util/str_tools.py      |  244 +++++++++++++++++++++++++++++++++++++++++++
 stem/util/term.py           |    3 +-
 test/unit/util/__init__.py  |    1 +
 test/unit/util/enum.py      |   16 ---
 test/unit/util/str_tools.py |   53 +++++++++
 7 files changed, 304 insertions(+), 39 deletions(-)

diff --git a/run_tests.py b/run_tests.py
index c891bf6..8cd393c 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -37,6 +37,7 @@ import test.unit.util.conf
 import test.unit.util.connection
 import test.unit.util.enum
 import test.unit.util.proc
+import test.unit.util.str_tools
 import test.unit.util.system
 import test.unit.util.tor_tools
 import test.unit.exit_policy.policy
@@ -114,6 +115,7 @@ UNIT_TESTS = (
   test.unit.util.connection.TestConnection,
   test.unit.util.conf.TestConf,
   test.unit.util.proc.TestProc,
+  test.unit.util.str_tools.TestStrTools,
   test.unit.util.system.TestSystem,
   test.unit.util.tor_tools.TestTorTools,
   test.unit.descriptor.export.TestExport,
diff --git a/stem/util/enum.py b/stem/util/enum.py
index 50f6728..72c4443 100644
--- a/stem/util/enum.py
+++ b/stem/util/enum.py
@@ -24,7 +24,6 @@ with overwritten string counterparts:
 
 ::
 
-  to_camel_case - converts a string to camel case
   UppercaseEnum - Provides an enum instance with capitalized values.
   Enum - Provides a basic, ordered  enumeration.
     |- keys - string representation of our enum keys
@@ -35,26 +34,7 @@ with overwritten string counterparts:
     +- __iter__ - iterator over our enum keys
 """
 
-def to_camel_case(label, word_divider = " "):
-  """
-  Converts the given string to camel case, ie:
-  
-  ::
-  
-    >>> to_camel_case("I_LIKE_PEPPERJACK!")
-    'I Like Pepperjack!'
-  
-  :param str label: input string to be converted
-  :param str word_divider: string used to replace underscores
-  """
-  
-  words = []
-  for entry in label.split("_"):
-    if len(entry) == 0: words.append("")
-    elif len(entry) == 1: words.append(entry.upper())
-    else: words.append(entry[0].upper() + entry[1:].lower())
-  
-  return word_divider.join(words)
+import stem.util.str_tools
 
 def UppercaseEnum(*args):
   """
@@ -86,7 +66,7 @@ class Enum(object):
     
     for entry in args:
       if isinstance(entry, str):
-        key, val = entry, to_camel_case(entry)
+        key, val = entry, stem.util.str_tools.to_camel_case(entry)
       elif isinstance(entry, tuple) and len(entry) == 2:
         key, val = entry
       else: raise ValueError("Unrecognized input: %s" % args)
diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py
new file mode 100644
index 0000000..b874425
--- /dev/null
+++ b/stem/util/str_tools.py
@@ -0,0 +1,244 @@
+"""
+Toolkit for various string activity.
+
+**Module Overview:**
+
+::
+
+  to_camel_case - converts a string to camel case
+  get_size_label - human readable label for a number of bytes
+  get_time_label - human readable label for a number of seconds
+  get_time_labels - human readable labels for each time unit
+  get_short_time_label - condensed time label output
+  parse_short_time_label - seconds represented by a short time label
+"""
+
+# label conversion tuples of the form...
+# (bits / bytes / seconds, short label, long label)
+SIZE_UNITS_BITS = (
+  (140737488355328.0, " Pb", " Petabit"),
+  (137438953472.0, " Tb", " Terabit"),
+  (134217728.0, " Gb", " Gigabit"),
+  (131072.0, " Mb", " Megabit"),
+  (128.0, " Kb", " Kilobit"),
+  (0.125, " b", " Bit"),
+)
+
+SIZE_UNITS_BYTES = (
+  (1125899906842624.0, " PB", " Petabyte"),
+  (1099511627776.0, " TB", " Terabyte"),
+  (1073741824.0, " GB", " Gigabyte"),
+  (1048576.0, " MB", " Megabyte"),
+  (1024.0, " KB", " Kilobyte"),
+  (1.0, " B", " Byte"),
+)
+
+TIME_UNITS = (
+  (86400.0, "d", " day"),
+  (3600.0, "h", " hour"),
+  (60.0, "m", " minute"),
+  (1.0, "s", " second"),
+)
+
+def to_camel_case(label, word_divider = " "):
+  """
+  Converts the given string to camel case, ie:
+  
+  ::
+  
+    >>> to_camel_case("I_LIKE_PEPPERJACK!")
+    'I Like Pepperjack!'
+  
+  :param str label: input string to be converted
+  :param str word_divider: string used to replace underscores
+  """
+  
+  words = []
+  for entry in label.split("_"):
+    if len(entry) == 0: words.append("")
+    elif len(entry) == 1: words.append(entry.upper())
+    else: words.append(entry[0].upper() + entry[1:].lower())
+  
+  return word_divider.join(words)
+
+def get_size_label(byte_count, decimal = 0, is_long = False, is_bytes = True):
+  """
+  Converts a number of bytes into a human readable label in its most
+  significant units. For instance, 7500 bytes would return "7 KB". If the
+  is_long option is used this expands unit labels to be the properly pluralized
+  full word (for instance 'Kilobytes' rather than 'KB'). Units go up through
+  petabytes.
+  
+  ::
+  
+    >>> get_size_label(2000000)
+    '1 MB'
+    
+    >>> get_size_label(1050, 2)
+    '1.02 KB'
+    
+    >>> get_size_label(1050, 3, True)
+    '1.025 Kilobytes'
+  
+  :param int byte_count: number of bytes to be converted
+  :param int decimal: number of decimal digits to be included
+  :param bool is_long: expands units label
+  :param bool is_bytes: provides units in bytes if true, bits otherwise
+  """
+  
+  if is_bytes: return _get_label(SIZE_UNITS_BYTES, byte_count, decimal, is_long)
+  else: return _get_label(SIZE_UNITS_BITS, byte_count, decimal, is_long)
+
+def get_time_label(seconds, decimal = 0, is_long = False):
+  """
+  Converts seconds into a time label truncated to its most significant units.
+  For instance, 7500 seconds would return "2h". Units go up through days.
+  
+  This defaults to presenting single character labels, but if the is_long
+  option is used this expands labels to be the full word (space included and
+  properly pluralized). For instance, "4h" would be "4 hours" and "1m" would
+  become "1 minute".
+  
+  ::
+  
+    >>> get_time_label(10000)
+    '2h'
+    
+    >>> get_time_label(61, 1, True)
+    '1.0 minute'
+    
+    >>> get_time_label(61, 2, True)
+    '1.01 minutes'
+  
+  :param int seconds: number of seconds to be converted
+  :param int decimal: number of decimal digits to be included
+  :param bool is_long: expands units label
+  """
+  
+  return _get_label(TIME_UNITS, seconds, decimal, is_long)
+
+def get_time_labels(seconds, is_long = False):
+  """
+  Provides a list of label conversions for each time unit, starting with its
+  most significant units on down. Any counts that evaluate to zero are omitted.
+  For example...
+  
+  ::
+  
+    >>> get_time_labels(400)
+    ['6m', '40s']
+    
+    >>> get_time_labels(3640, True)
+    ['1 hour', '40 seconds']
+  
+  :param int seconds: number of seconds to be converted
+  :param bool is_long: expands units label
+  """
+  
+  time_labels = []
+  
+  for count_per_unit, _, _ in TIME_UNITS:
+    if seconds >= count_per_unit:
+      time_labels.append(_get_label(TIME_UNITS, seconds, 0, is_long))
+      seconds %= count_per_unit
+  
+  return time_labels
+
+def get_short_time_label(seconds):
+  """
+  Provides a time in the following format:
+  [[dd-]hh:]mm:ss
+  
+  :param int seconds: number of seconds to be converted
+  """
+  
+  time_comp = {}
+  
+  for amount, _, label in TIME_UNITS:
+    count = int(seconds / amount)
+    seconds %= amount
+    time_comp[label.strip()] = count
+  
+  label = "%02i:%02i" % (time_comp["minute"], time_comp["second"])
+  
+  if time_comp["day"]:
+    label = "%i-%02i:%s" % (time_comp["day"], time_comp["hour"], label)
+  elif time_comp["hour"]:
+    label = "%02i:%s" % (time_comp["hour"], label)
+  
+  return label
+
+def parse_short_time_label(label):
+  """
+  Provides the number of seconds corresponding to the formatting used for the
+  cputime and etime fields of ps:
+  [[dd-]hh:]mm:ss or mm:ss.ss
+  
+  :param str label: time entry to be parsed
+  
+  :raises: ValueError if input is malformed
+  """
+  
+  days, hours, minutes, seconds = '0', '0', '0', '0'
+  
+  if '-' in label:
+    days, label = label.split('-', 1)
+  
+  time_comp = label.split(":")
+  
+  if len(time_comp) == 3:
+    hours, minutes, seconds = time_comp
+  elif len(time_comp) == 2:
+    minutes, seconds = time_comp
+  else:
+    raise ValueError("Invalid time format, we expected '[[dd-]hh:]mm:ss' or 'mm:ss.ss': %s" % label)
+  
+  try:
+    time_sum = int(float(seconds))
+    time_sum += int(minutes) * 60
+    time_sum += int(hours) * 3600
+    time_sum += int(days) * 86400
+    return time_sum
+  except ValueError:
+    raise ValueError("Non-numeric value in time entry: %s" % label)
+
+def _get_label(units, count, decimal, is_long):
+  """
+  Provides label corresponding to units of the highest significance in the
+  provided set. This rounds down (ie, integer truncation after visible units).
+  
+  :param tuple units: type of units to be used for conversion, containing (count_per_unit, short_label, long_label)
+  :param int count: number of base units being converted
+  :param int decimal: decimal precision of label
+  :param bool is_long: uses the long label if true, short label otherwise
+  """
+  
+  # formatted string for the requested number of digits
+  label_format = "%%.%if" % decimal
+  
+  # for zero or negative values use the smallest units
+  if count < 1:
+    units_label = units[-1][2] + "s" if is_long else units[-1][1]
+    return "%s%s" % (label_format % count, units_label)
+  
+  for count_per_unit, short_label, long_label in units:
+    if count >= count_per_unit:
+      # Rounding down with a '%f' is a little clunky. Reducing the count so
+      # it'll divide evenly as the rounded down value.
+      
+      count -= count % (count_per_unit / (10 ** decimal))
+      count_label = label_format % (count / count_per_unit)
+      
+      if is_long:
+        # Pluralize if any of the visible units make it greater than one. For
+        # instance 1.0003 is plural but 1.000 isn't.
+        
+        if decimal > 0:
+          is_plural = count > count_per_unit
+        else:
+          is_plural = count >= count_per_unit * 2
+        
+        return count_label + long_label + ("s" if is_plural else "")
+      else:
+        return count_label + short_label
+
diff --git a/stem/util/term.py b/stem/util/term.py
index fd4c22e..556b481 100644
--- a/stem/util/term.py
+++ b/stem/util/term.py
@@ -3,6 +3,7 @@ Utilities for working with the terminal.
 """
 
 import stem.util.enum
+import stem.util.str_tools
 
 TERM_COLORS = ("BLACK", "RED", "GREEN", "YELLOW", "BLUE", "MAGENTA", "CYAN", "WHITE")
 
@@ -41,7 +42,7 @@ def format(msg, *attr):
   
   encodings = []
   for text_attr in attr:
-    text_attr, encoding = stem.util.enum.to_camel_case(text_attr), None
+    text_attr, encoding = stem.util.str_tools.to_camel_case(text_attr), None
     encoding = FG_ENCODING.get(text_attr, encoding)
     encoding = BG_ENCODING.get(text_attr, encoding)
     encoding = ATTR_ENCODING.get(text_attr, encoding)
diff --git a/test/unit/util/__init__.py b/test/unit/util/__init__.py
index d0eeda3..33b81d3 100644
--- a/test/unit/util/__init__.py
+++ b/test/unit/util/__init__.py
@@ -7,6 +7,7 @@ __all__ = [
   "connection",
   "enum",
   "proc",
+  "str_tools",
   "system",
   "tor_tools",
 ]
diff --git a/test/unit/util/enum.py b/test/unit/util/enum.py
index 5a28f62..88469dd 100644
--- a/test/unit/util/enum.py
+++ b/test/unit/util/enum.py
@@ -6,22 +6,6 @@ import unittest
 import stem.util.enum
 
 class TestEnum(unittest.TestCase):
-  def test_to_camel_case(self):
-    """
-    Checks the stem.util.enum.to_camel_case function.
-    """
-    
-    # test the pydoc example
-    self.assertEquals("I Like Pepperjack!", stem.util.enum.to_camel_case("I_LIKE_PEPPERJACK!"))
-    
-    # check a few edge cases
-    self.assertEquals("", stem.util.enum.to_camel_case(""))
-    self.assertEquals("Hello", stem.util.enum.to_camel_case("hello"))
-    self.assertEquals("Hello", stem.util.enum.to_camel_case("HELLO"))
-    self.assertEquals("Hello  World", stem.util.enum.to_camel_case("hello__world"))
-    self.assertEquals("Hello\tworld", stem.util.enum.to_camel_case("hello\tWORLD"))
-    self.assertEquals("Hello\t\tWorld", stem.util.enum.to_camel_case("hello__world", "\t"))
-  
   def test_enum_examples(self):
     """
     Checks that the pydoc examples are accurate.
diff --git a/test/unit/util/str_tools.py b/test/unit/util/str_tools.py
new file mode 100644
index 0000000..ce5539f
--- /dev/null
+++ b/test/unit/util/str_tools.py
@@ -0,0 +1,53 @@
+"""
+Unit tests for the stem.util.str_tools functions.
+"""
+
+import unittest
+from stem.util import str_tools
+
+class TestStrTools(unittest.TestCase):
+  def test_to_camel_case(self):
+    """
+    Checks the to_camel_case() function.
+    """
+    
+    # test the pydoc example
+    self.assertEquals("I Like Pepperjack!", str_tools.to_camel_case("I_LIKE_PEPPERJACK!"))
+    
+    # check a few edge cases
+    self.assertEquals("", str_tools.to_camel_case(""))
+    self.assertEquals("Hello", str_tools.to_camel_case("hello"))
+    self.assertEquals("Hello", str_tools.to_camel_case("HELLO"))
+    self.assertEquals("Hello  World", str_tools.to_camel_case("hello__world"))
+    self.assertEquals("Hello\tworld", str_tools.to_camel_case("hello\tWORLD"))
+    self.assertEquals("Hello\t\tWorld", str_tools.to_camel_case("hello__world", "\t"))
+  
+  def test_get_size_label(self):
+    """
+    Checks the get_size_label() function.
+    """
+    
+    # test the pydoc examples
+    self.assertEquals('1 MB', str_tools.get_size_label(2000000))
+    self.assertEquals('1.02 KB', str_tools.get_size_label(1050, 2))
+    self.assertEquals('1.025 Kilobytes', str_tools.get_size_label(1050, 3, True))
+  
+  def test_get_time_label(self):
+    """
+    Checks the get_time_label() function.
+    """
+    
+    # test the pydoc examples
+    self.assertEquals('2h', str_tools.get_time_label(10000))
+    self.assertEquals('1.0 minute', str_tools.get_time_label(61, 1, True))
+    self.assertEquals('1.01 minutes', str_tools.get_time_label(61, 2, True))
+  
+  def test_get_time_labels(self):
+    """
+    Checks the get_time_labels() function.
+    """
+    
+    # test the pydoc examples
+    self.assertEquals(['6m', '40s'], str_tools.get_time_labels(400))
+    self.assertEquals(['1 hour', '40 seconds'], str_tools.get_time_labels(3640, True))
+





More information about the tor-commits mailing list