44
55"""An abstraction for maintaining job runtime and its units."""
66
7- import unittest
8- from copy import copy
9-
10-
11- class JobTime :
12- # Possible units.
13- units = ["h" , "m" , "s" , "ms" , "us" , "ns" , "ps" , "fs" ]
14- dividers = [
15- 60.0 ,
16- ] * 3 + [
17- 1000.0 ,
18- ] * 5
7+ __all__ = ("JobTime" ,)
8+
9+ # Supported time units and the divisor required to convert the value in the
10+ # current unit to the next largest unit.
11+ TIME_UNITS = ["h" , "m" , "s" , "ms" , "us" , "ns" , "ps" , "fs" ]
12+ TIME_DIVIDERS = [60.0 ] * 3 + [1000.0 ] * 5
13+
14+
15+ # TODO: Migrate to Time instead of a custom implementation
16+ class JobTime : # noqa: PLW1641 # Muitable object should not implement __hash__
17+ """Job runtime."""
18+
19+ def __init__ (self , time : float = 0.0 , unit : str = "s" , * , normalize : bool = True ) -> None :
20+ """Initialise."""
21+ self ._time : float = time
22+ self ._unit : str = unit
23+
24+ self .set (
25+ time = time ,
26+ unit = unit ,
27+ normalize = normalize ,
28+ )
29+
30+ @property
31+ def time (self ) -> float :
32+ """Get time."""
33+ return self ._time
34+
35+ @property
36+ def unit (self ) -> str :
37+ """Get unit."""
38+ return self ._unit
39+
40+ @staticmethod
41+ def _check_valid_unit (unit : str ) -> None :
42+ """Assert unit is valid."""
43+ if unit not in TIME_UNITS :
44+ msg = f"unit '{ unit } ' is not a supported time unit: { TIME_UNITS } "
45+ raise KeyError (msg )
46+
47+ def set (self , time : float , unit : str , * , normalize : bool = True ) -> None :
48+ """Public API to set the instance variables time, unit."""
49+ self ._check_valid_unit (unit = unit )
1950
20- def __init__ ( self , time : float = 0.0 , unit : str = "s" , normalize : bool = True ) -> None :
21- self .set ( time , unit , normalize )
51+ self . _time = time
52+ self ._unit = unit
2253
23- def set (self , time : float , unit : str , normalize : bool = True ) -> None :
24- """Public API to set the instance variables time, unit."""
25- self .__time = time
26- self .__unit = unit
27- assert self .__unit in self .units
2854 if normalize :
2955 self ._normalize ()
3056
3157 def get (self ) -> tuple [float , str ]:
32- """Returns the time and unit as a tuple."""
33- return self .__time , self .__unit
58+ """Return the time and unit as a tuple."""
59+ return self ._time , self ._unit
3460
35- def with_unit (self , unit : str ):
36- """Return a copy of this object that has a specific unit and a value
37- scaled accordingly.
61+ def with_unit (self , unit : str ) -> "JobTime" :
62+ """Return a copy with the given unit.
3863
3964 Note that the scaling may not be lossless due to rounding errors and
4065 limited precision.
4166 """
42- target_index = self .units .index (unit )
43- index = self .units .index (self .__unit )
44- jt = copy (self )
67+ self ._check_valid_unit (unit = unit )
68+
69+ target_index = TIME_UNITS .index (unit )
70+ index = TIME_UNITS .index (self ._unit )
71+
72+ new_time = self ._time
4573 while index < target_index :
4674 index += 1
47- jt . __time *= self . dividers [index ]
48- jt . __unit = self . units [ index ]
75+ new_time *= TIME_DIVIDERS [index ]
76+
4977 while index > target_index :
50- jt . __time /= self . dividers [index ]
78+ new_time /= TIME_DIVIDERS [index ]
5179 index -= 1
52- jt .__unit = self .units [index ]
53- return jt
80+
81+ return JobTime (
82+ time = new_time ,
83+ unit = unit ,
84+ normalize = False ,
85+ )
5486
5587 def _normalize (self ) -> None :
5688 """Brings the time and its units to a more meaningful magnitude.
@@ -62,68 +94,81 @@ def _normalize(self) -> None:
6294 23434s -> 6.509h
6395
6496 The supported magnitudes and their associated divider values are
65- provided by JobTime.units and JobTime.dividers .
97+ provided by TIME_UNITS and TIME_DIVIDERS .
6698 """
67- if self .__time == 0 :
99+ if self ._time == 0 :
68100 return
69101
70- index = self .units .index (self .__unit )
71- normalized_time = self .__time
72- while index > 0 and normalized_time >= self .dividers [index ]:
73- normalized_time = normalized_time / self .dividers [index ]
102+ index = TIME_UNITS .index (self ._unit )
103+ normalized_time = self ._time
104+
105+ while index > 0 and normalized_time >= TIME_DIVIDERS [index ]:
106+ normalized_time = normalized_time / TIME_DIVIDERS [index ]
74107 index = index - 1
75- self .__time = normalized_time
76- self .__unit = self .units [index ]
108+
109+ self ._time = normalized_time
110+ self ._unit = TIME_UNITS [index ]
111+
112+ def _with_common_units (self , other : "JobTime" ) -> tuple [float , float ]:
113+ """Convert times to common units for relative comparison.
114+
115+ Returns:
116+ tuple of both times as floats in the smallest unit to be
117+ used for comparison.
118+
119+ """
120+ stime = self ._time
121+ otime = other .time
122+
123+ sidx = TIME_UNITS .index (self ._unit )
124+ oidx = TIME_UNITS .index (other .unit )
125+
126+ # If the time units are not the same
127+ if sidx != oidx :
128+ # Pick the smallest unit and standardise on that unit. This means
129+ # the comparison is less likely to be lossy.
130+ if sidx < oidx :
131+ stime = self .with_unit (other .unit ).time
132+ else :
133+ otime = other .with_unit (self .unit ).time
134+
135+ return stime , otime
77136
78137 def __str__ (self ) -> str :
79- """Indicates <time><unit> as string .
138+ """Time as a string <time><unit>.
80139
81140 The time value is truncated to 3 decimal places.
82141 Returns an empty string if the __time is set to 0.
83142 """
84- if self .__time == 0 :
143+ if self ._time == 0 :
85144 return ""
86- return f"{ self .__time :.3f} { self .__unit } "
87-
88- def __eq__ (self , other ) -> bool :
89- assert isinstance (other , JobTime )
90- other_time , other_unit = other .get ()
91- return self .__unit == other_unit and self .__time == other_time
145+ return f"{ self ._time :.3f} { self ._unit } "
92146
93- def __gt__ (self , other ) -> bool :
94- if self .__time == 0 :
147+ def __eq__ (self , other : object ) -> bool :
148+ """Check equality."""
149+ if not isinstance (other , JobTime ):
95150 return False
96151
97- assert isinstance (other , JobTime )
98- other_time , other_unit = other .get ()
99- if other_time == 0 :
100- return True
152+ stime , otime = self ._with_common_units (other = other )
101153
102- sidx = JobTime .units .index (self .__unit )
103- oidx = JobTime .units .index (other_unit )
104- if sidx < oidx :
105- return True
106- if sidx > oidx :
107- return False
108- return self .__time > other_time
109-
110-
111- class TestJobTimeMethods (unittest .TestCase ):
112- def test_with_unit (self ) -> None :
113- # First data set
114- h = JobTime (6 , "h" , normalize = False )
115- m = JobTime (360 , "m" , normalize = False )
116- s = JobTime (21600 , "s" , normalize = False )
117- ms = JobTime (21600000 , "ms" , normalize = False )
118- for src in [h , m , s , ms ]:
119- for unit , dst in [("h" , h ), ("m" , m ), ("s" , s ), ("ms" , ms )]:
120- assert src .with_unit (unit ) == dst
121- # Second data set
122- fs = JobTime (123456000000 , "fs" , normalize = False )
123- ps = JobTime (123456000 , "ps" , normalize = False )
124- ns = JobTime (123456 , "ns" , normalize = False )
125- us = JobTime (123.456 , "us" , normalize = False )
126- ms = JobTime (0.123456 , "ms" , normalize = False )
127- for src in [fs , ps , ns , us , ms ]:
128- for unit , dst in [("fs" , fs ), ("ps" , ps ), ("ns" , ns ), ("us" , us ), ("ms" , ms )]:
129- assert src .with_unit (unit ) == dst
154+ return stime == otime
155+
156+ def __gt__ (self , other : object ) -> bool :
157+ """Check time is greater than."""
158+ if not isinstance (other , JobTime ):
159+ msg = f"Can't compare { self } with { other } "
160+ raise TypeError (msg )
161+
162+ stime , otime = self ._with_common_units (other = other )
163+
164+ return stime > otime
165+
166+ def __ge__ (self , other : object ) -> bool :
167+ """Check time is greater than or equal to."""
168+ if not isinstance (other , JobTime ):
169+ msg = f"Can't compare { self } with { other } "
170+ raise TypeError (msg )
171+
172+ stime , otime = self ._with_common_units (other = other )
173+
174+ return stime >= otime
0 commit comments