Browse Source

Add Duration type (with units)

Joeri Exelmans 5 years ago
parent
commit
0924338960
2 changed files with 183 additions and 0 deletions
  1. 116 0
      src/sccd/util/duration.py
  2. 67 0
      src/sccd/util/test_duration.py

+ 116 - 0
src/sccd/util/duration.py

@@ -0,0 +1,116 @@
+from enum import *
+from dataclasses import *
+from typing import *
+import math
+
+@dataclass
+class _Unit:
+  notation: str
+  relative_size: int
+  larger: Optional[Tuple[Any, int]] = None
+  # smaller: Optional[Tuple[Any, int]] = None
+
+  def __eq__(self, other):
+    return self is other
+
+FemtoSecond = _Unit("fs", 1)
+PicoSecond = _Unit("ps", 1000)
+Nanosecond = _Unit("ns", 1000000)
+Microsecond = _Unit("µs", 1000000000)
+Millisecond = _Unit("ms", 1000000000000)
+Second = _Unit("s", 1000000000000000)
+Minute = _Unit("m", 60000000000000000)
+Hour = _Unit("h", 3600000000000000000)
+Day = _Unit("D", 86400000000000000000)
+
+FemtoSecond.larger = (PicoSecond, 1000)
+PicoSecond.larger = (Nanosecond, 1000)
+Nanosecond.larger = (Microsecond, 1000)
+Microsecond.larger = (Millisecond, 1000)
+Millisecond.larger = (Second, 1000)
+Second.larger = (Minute, 60)
+Minute.larger = (Hour, 60)
+Hour.larger = (Day, 24)
+
+# Day.smaller = (Hour, 24)
+# Hour.smaller = (Minute, 60)
+# Minute.smaller = (Second, 60)
+# Second.smaller = (Millisecond, 1000)
+# Millisecond.smaller = (Microsecond, 1000)
+# Microsecond.smaller = (Nanosecond, 1000)
+# Nanosecond.smaller = (PicoSecond, 1000)
+# PicoSecond.smaller = (FemtoSecond, 1000)
+
+
+# @dataclass
+class Duration:
+  def __init__(self, val: int, unit: _Unit = None):
+    self.val = val
+    self.unit = unit
+    if self.val != 0 and self.unit is None:
+      raise Exception("Duration: Non-zero value should have unit")
+    # Zero-durations are treated a bit special
+    if self.val == 0 and self.unit is not None:
+      raise Exception("Duration: Zero value should not have unit")
+
+  # Can only convert to smaller units.
+  # Returns new Duration.
+  def convert(self, unit: _Unit):
+    if self.unit is None:
+      return self
+
+    # Precondition
+    assert self.unit.relative_size >= unit.relative_size
+    factor = self.unit.relative_size // unit.relative_size
+    return Duration(self.val * factor, unit)
+
+  # Convert Duration to the largest possible unit.
+  # Returns new Duration.
+  def normalize(self):
+    if self.unit is None:
+      return self
+
+    val = self.val
+    unit = self.unit
+    next_unit, factor = unit.larger
+    while val % factor == 0:
+      val //= factor
+      unit = next_unit
+      next_unit, factor = unit.larger
+    return Duration(val, unit)
+
+  def __str__(self):
+    if self.unit is None:
+      return '0'
+    return str(self.val)+' '+self.unit.notation
+
+  def __repr__(self):
+    return "Duration("+self.__str__()+")"
+
+  def __eq__(self, other):
+    return self.val == other.val and self.unit is other.unit
+
+def gcd_pair(x: Duration, y: Duration) -> Duration:
+  if x.unit is None:
+    return y
+  if y.unit is None:
+    return x
+
+  if x.unit.relative_size >= y.unit.relative_size:
+    x_converted = x.convert(y.unit)
+    y_converted = y
+  else:
+    x_converted = x
+    y_converted = y.convert(x.unit)
+  # x_conv and y_conv are now the same unit
+  gcd = math.gcd(x_converted.val, y_converted.val)
+  return Duration(gcd, x_converted.unit).normalize()
+
+def gcd(*iterable) -> Duration:
+  g = None
+  for d in iterable:
+    if g is None:
+      g = d
+    else:
+      g = gcd_pair(g, d)
+  return g

+ 67 - 0
src/sccd/util/test_duration.py

@@ -0,0 +1,67 @@
+import unittest
+from duration import *
+
+
+class TestDuration(unittest.TestCase):
+
+  def test_equal(self):
+    # The same amount of time, but objects not considered equal.
+    x = Duration(1000, Millisecond)
+    y = Duration(1, Second)
+
+    self.assertNotEqual(x, y)
+
+    self.assertEqual(x.normalize(), y.normalize())
+
+    # original objects left intact by normalize() operation
+    self.assertNotEqual(x, y)
+
+  def test_convert_unit(self):
+    x = Duration(2, Second)
+
+    x2 = x.convert(Microsecond)
+
+    self.assertEqual(x2, Duration(2000000, Microsecond))
+
+  def test_convert_zero(self):
+    x = Duration(0)
+
+    x2 = x.convert(Millisecond)
+
+    self.assertEqual(x2, Duration(0))
+
+  def test_normalize(self):
+    x = Duration(1000, Millisecond)
+    y = Duration(1000000, Microsecond)
+    z = Duration(0)
+
+    self.assertEqual(x.normalize(), Duration(1, Second))
+    self.assertEqual(y.normalize(), Duration(1, Second))
+    self.assertEqual(z.normalize(), Duration(0))
+
+  def test_gcd(self):
+    x = Duration(20, Second)
+    y = Duration(100, Microsecond)
+
+    u = gcd(x, y)
+    v = gcd(y, x)
+
+    self.assertEqual(u, Duration(100, Microsecond))
+    self.assertEqual(v, Duration(100, Microsecond))
+
+  def test_gcd_zero(self):
+    x = Duration(0)
+    y = Duration(100, Microsecond)
+
+    u = gcd(x, y)
+    v = gcd(y, x)
+
+    self.assertEqual(u, Duration(100, Microsecond))
+    self.assertEqual(v, Duration(100, Microsecond))
+
+  def test_gcd_many(self):
+    l = [Duration(3, Microsecond), Duration(0), Duration(10, Millisecond)]
+
+    u = gcd(*l)
+
+    self.assertEqual(u, Duration(1, Microsecond))