1# Copyright 2008-2015 Nokia Networks 2# Copyright 2016- Robot Framework Foundation 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import datetime 17import time 18import re 19 20from .normalizing import normalize 21from .misc import plural_or_not, roundup 22from .robottypes import is_number, is_string 23 24 25_timer_re = re.compile(r'^([+-])?(\d+:)?(\d+):(\d+)(\.\d+)?$') 26 27 28def _get_timetuple(epoch_secs=None): 29 if epoch_secs is None: # can also be 0 (at least in unit tests) 30 epoch_secs = time.time() 31 secs, millis = _float_secs_to_secs_and_millis(epoch_secs) 32 timetuple = time.localtime(secs)[:6] # from year to secs 33 return timetuple + (millis,) 34 35def _float_secs_to_secs_and_millis(secs): 36 isecs = int(secs) 37 millis = roundup((secs - isecs) * 1000) 38 return (isecs, millis) if millis < 1000 else (isecs+1, 0) 39 40 41def timestr_to_secs(timestr, round_to=3): 42 """Parses time like '1h 10s', '01:00:10' or '42' and returns seconds.""" 43 if is_string(timestr) or is_number(timestr): 44 for converter in _number_to_secs, _timer_to_secs, _time_string_to_secs: 45 secs = converter(timestr) 46 if secs is not None: 47 return secs if round_to is None else roundup(secs, round_to) 48 raise ValueError("Invalid time string '%s'." % timestr) 49 50def _number_to_secs(number): 51 try: 52 return float(number) 53 except ValueError: 54 return None 55 56def _timer_to_secs(number): 57 match = _timer_re.match(number) 58 if not match: 59 return None 60 prefix, hours, minutes, seconds, millis = match.groups() 61 seconds = float(minutes) * 60 + float(seconds) 62 if hours: 63 seconds += float(hours[:-1]) * 60 * 60 64 if millis: 65 seconds += float(millis[1:]) / 10**len(millis[1:]) 66 if prefix == '-': 67 seconds *= -1 68 return seconds 69 70def _time_string_to_secs(timestr): 71 timestr = _normalize_timestr(timestr) 72 if not timestr: 73 return None 74 millis = secs = mins = hours = days = 0 75 if timestr[0] == '-': 76 sign = -1 77 timestr = timestr[1:] 78 else: 79 sign = 1 80 temp = [] 81 for c in timestr: 82 try: 83 if c == 'x': millis = float(''.join(temp)); temp = [] 84 elif c == 's': secs = float(''.join(temp)); temp = [] 85 elif c == 'm': mins = float(''.join(temp)); temp = [] 86 elif c == 'h': hours = float(''.join(temp)); temp = [] 87 elif c == 'd': days = float(''.join(temp)); temp = [] 88 else: temp.append(c) 89 except ValueError: 90 return None 91 if temp: 92 return None 93 return sign * (millis/1000 + secs + mins*60 + hours*60*60 + days*60*60*24) 94 95def _normalize_timestr(timestr): 96 timestr = normalize(timestr) 97 for specifier, aliases in [('x', ['millisecond', 'millisec', 'millis', 98 'msec', 'ms']), 99 ('s', ['second', 'sec']), 100 ('m', ['minute', 'min']), 101 ('h', ['hour']), 102 ('d', ['day'])]: 103 plural_aliases = [a+'s' for a in aliases if not a.endswith('s')] 104 for alias in plural_aliases + aliases: 105 if alias in timestr: 106 timestr = timestr.replace(alias, specifier) 107 return timestr 108 109 110def secs_to_timestr(secs, compact=False): 111 """Converts time in seconds to a string representation. 112 113 Returned string is in format like 114 '1 day 2 hours 3 minutes 4 seconds 5 milliseconds' with following rules: 115 116 - Time parts having zero value are not included (e.g. '3 minutes 4 seconds' 117 instead of '0 days 0 hours 3 minutes 4 seconds') 118 - Hour part has a maximun of 23 and minutes and seconds both have 59 119 (e.g. '1 minute 40 seconds' instead of '100 seconds') 120 121 If compact has value 'True', short suffixes are used. 122 (e.g. 1d 2h 3min 4s 5ms) 123 """ 124 return _SecsToTimestrHelper(secs, compact).get_value() 125 126 127class _SecsToTimestrHelper: 128 129 def __init__(self, float_secs, compact): 130 self._compact = compact 131 self._ret = [] 132 self._sign, millis, secs, mins, hours, days \ 133 = self._secs_to_components(float_secs) 134 self._add_item(days, 'd', 'day') 135 self._add_item(hours, 'h', 'hour') 136 self._add_item(mins, 'min', 'minute') 137 self._add_item(secs, 's', 'second') 138 self._add_item(millis, 'ms', 'millisecond') 139 140 def get_value(self): 141 if len(self._ret) > 0: 142 return self._sign + ' '.join(self._ret) 143 return '0s' if self._compact else '0 seconds' 144 145 def _add_item(self, value, compact_suffix, long_suffix): 146 if value == 0: 147 return 148 if self._compact: 149 suffix = compact_suffix 150 else: 151 suffix = ' %s%s' % (long_suffix, plural_or_not(value)) 152 self._ret.append('%d%s' % (value, suffix)) 153 154 def _secs_to_components(self, float_secs): 155 if float_secs < 0: 156 sign = '- ' 157 float_secs = abs(float_secs) 158 else: 159 sign = '' 160 int_secs, millis = _float_secs_to_secs_and_millis(float_secs) 161 secs = int_secs % 60 162 mins = int_secs // 60 % 60 163 hours = int_secs // (60 * 60) % 24 164 days = int_secs // (60 * 60 * 24) 165 return sign, millis, secs, mins, hours, days 166 167 168def format_time(timetuple_or_epochsecs, daysep='', daytimesep=' ', timesep=':', 169 millissep=None): 170 """Returns a timestamp formatted from given time using separators. 171 172 Time can be given either as a timetuple or seconds after epoch. 173 174 Timetuple is (year, month, day, hour, min, sec[, millis]), where parts must 175 be integers and millis is required only when millissep is not None. 176 Notice that this is not 100% compatible with standard Python timetuples 177 which do not have millis. 178 179 Seconds after epoch can be either an integer or a float. 180 """ 181 if is_number(timetuple_or_epochsecs): 182 timetuple = _get_timetuple(timetuple_or_epochsecs) 183 else: 184 timetuple = timetuple_or_epochsecs 185 daytimeparts = ['%02d' % t for t in timetuple[:6]] 186 day = daysep.join(daytimeparts[:3]) 187 time_ = timesep.join(daytimeparts[3:6]) 188 millis = millissep and '%s%03d' % (millissep, timetuple[6]) or '' 189 return day + daytimesep + time_ + millis 190 191 192def get_time(format='timestamp', time_=None): 193 """Return the given or current time in requested format. 194 195 If time is not given, current time is used. How time is returned is 196 is deternined based on the given 'format' string as follows. Note that all 197 checks are case insensitive. 198 199 - If 'format' contains word 'epoch' the time is returned in seconds after 200 the unix epoch. 201 - If 'format' contains any of the words 'year', 'month', 'day', 'hour', 202 'min' or 'sec' only selected parts are returned. The order of the returned 203 parts is always the one in previous sentence and order of words in 204 'format' is not significant. Parts are returned as zero padded strings 205 (e.g. May -> '05'). 206 - Otherwise (and by default) the time is returned as a timestamp string in 207 format '2006-02-24 15:08:31' 208 """ 209 time_ = int(time_ or time.time()) 210 format = format.lower() 211 # 1) Return time in seconds since epoc 212 if 'epoch' in format: 213 return time_ 214 timetuple = time.localtime(time_) 215 parts = [] 216 for i, match in enumerate('year month day hour min sec'.split()): 217 if match in format: 218 parts.append('%.2d' % timetuple[i]) 219 # 2) Return time as timestamp 220 if not parts: 221 return format_time(timetuple, daysep='-') 222 # Return requested parts of the time 223 elif len(parts) == 1: 224 return parts[0] 225 else: 226 return parts 227 228 229def parse_time(timestr): 230 """Parses the time string and returns its value as seconds since epoch. 231 232 Time can be given in five different formats: 233 234 1) Numbers are interpreted as time since epoch directly. It is possible to 235 use also ints and floats, not only strings containing numbers. 236 2) Valid timestamp ('YYYY-MM-DD hh:mm:ss' and 'YYYYMMDD hhmmss'). 237 3) 'NOW' (case-insensitive) is the current local time. 238 4) 'UTC' (case-insensitive) is the current time in UTC. 239 5) Format 'NOW - 1 day' or 'UTC + 1 hour 30 min' is the current local/UTC 240 time plus/minus the time specified with the time string. 241 242 Seconds are rounded down to avoid getting times in the future. 243 """ 244 for method in [_parse_time_epoch, 245 _parse_time_timestamp, 246 _parse_time_now_and_utc]: 247 seconds = method(timestr) 248 if seconds is not None: 249 return int(seconds) 250 raise ValueError("Invalid time format '%s'." % timestr) 251 252def _parse_time_epoch(timestr): 253 try: 254 ret = float(timestr) 255 except ValueError: 256 return None 257 if ret < 0: 258 raise ValueError("Epoch time must be positive (got %s)." % timestr) 259 return ret 260 261def _parse_time_timestamp(timestr): 262 try: 263 return timestamp_to_secs(timestr, (' ', ':', '-', '.')) 264 except ValueError: 265 return None 266 267def _parse_time_now_and_utc(timestr): 268 timestr = timestr.replace(' ', '').lower() 269 base = _parse_time_now_and_utc_base(timestr[:3]) 270 if base is not None: 271 extra = _parse_time_now_and_utc_extra(timestr[3:]) 272 if extra is not None: 273 return base + extra 274 return None 275 276def _parse_time_now_and_utc_base(base): 277 now = time.time() 278 if base == 'now': 279 return now 280 if base == 'utc': 281 zone = time.altzone if time.localtime().tm_isdst else time.timezone 282 return now + zone 283 return None 284 285def _parse_time_now_and_utc_extra(extra): 286 if not extra: 287 return 0 288 if extra[0] not in ['+', '-']: 289 return None 290 return (1 if extra[0] == '+' else -1) * timestr_to_secs(extra[1:]) 291 292 293def get_timestamp(daysep='', daytimesep=' ', timesep=':', millissep='.'): 294 return TIMESTAMP_CACHE.get_timestamp(daysep, daytimesep, timesep, millissep) 295 296 297def timestamp_to_secs(timestamp, seps=None): 298 try: 299 secs = _timestamp_to_millis(timestamp, seps) / 1000.0 300 except (ValueError, OverflowError): 301 raise ValueError("Invalid timestamp '%s'." % timestamp) 302 else: 303 return roundup(secs, 3) 304 305 306def secs_to_timestamp(secs, seps=None, millis=False): 307 if not seps: 308 seps = ('', ' ', ':', '.' if millis else None) 309 ttuple = time.localtime(secs)[:6] 310 if millis: 311 millis = (secs - int(secs)) * 1000 312 ttuple = ttuple + (roundup(millis),) 313 return format_time(ttuple, *seps) 314 315 316def get_elapsed_time(start_time, end_time): 317 """Returns the time between given timestamps in milliseconds.""" 318 if start_time == end_time or not (start_time and end_time): 319 return 0 320 if start_time[:-4] == end_time[:-4]: 321 return int(end_time[-3:]) - int(start_time[-3:]) 322 start_millis = _timestamp_to_millis(start_time) 323 end_millis = _timestamp_to_millis(end_time) 324 # start/end_millis can be long but we want to return int when possible 325 return int(end_millis - start_millis) 326 327 328def elapsed_time_to_string(elapsed, include_millis=True): 329 """Converts elapsed time in milliseconds to format 'hh:mm:ss.mil'. 330 331 If `include_millis` is True, '.mil' part is omitted. 332 """ 333 prefix = '' 334 if elapsed < 0: 335 prefix = '-' 336 elapsed = abs(elapsed) 337 if include_millis: 338 return prefix + _elapsed_time_to_string(elapsed) 339 return prefix + _elapsed_time_to_string_without_millis(elapsed) 340 341def _elapsed_time_to_string(elapsed): 342 secs, millis = divmod(roundup(elapsed), 1000) 343 mins, secs = divmod(secs, 60) 344 hours, mins = divmod(mins, 60) 345 return '%02d:%02d:%02d.%03d' % (hours, mins, secs, millis) 346 347def _elapsed_time_to_string_without_millis(elapsed): 348 secs = roundup(elapsed, ndigits=-3) // 1000 349 mins, secs = divmod(secs, 60) 350 hours, mins = divmod(mins, 60) 351 return '%02d:%02d:%02d' % (hours, mins, secs) 352 353 354def _timestamp_to_millis(timestamp, seps=None): 355 if seps: 356 timestamp = _normalize_timestamp(timestamp, seps) 357 Y, M, D, h, m, s, millis = _split_timestamp(timestamp) 358 secs = time.mktime(datetime.datetime(Y, M, D, h, m, s).timetuple()) 359 return roundup(1000*secs + millis) 360 361def _normalize_timestamp(ts, seps): 362 for sep in seps: 363 if sep in ts: 364 ts = ts.replace(sep, '') 365 ts = ts.ljust(17, '0') 366 return '%s%s%s %s:%s:%s.%s' % (ts[:4], ts[4:6], ts[6:8], ts[8:10], 367 ts[10:12], ts[12:14], ts[14:17]) 368 369def _split_timestamp(timestamp): 370 years = int(timestamp[:4]) 371 mons = int(timestamp[4:6]) 372 days = int(timestamp[6:8]) 373 hours = int(timestamp[9:11]) 374 mins = int(timestamp[12:14]) 375 secs = int(timestamp[15:17]) 376 millis = int(timestamp[18:21]) 377 return years, mons, days, hours, mins, secs, millis 378 379 380class TimestampCache(object): 381 382 def __init__(self): 383 self._previous_secs = None 384 self._previous_separators = None 385 self._previous_timestamp = None 386 387 def get_timestamp(self, daysep='', daytimesep=' ', timesep=':', millissep='.'): 388 epoch = self._get_epoch() 389 secs, millis = _float_secs_to_secs_and_millis(epoch) 390 if self._use_cache(secs, daysep, daytimesep, timesep): 391 return self._cached_timestamp(millis, millissep) 392 timestamp = format_time(epoch, daysep, daytimesep, timesep, millissep) 393 self._cache_timestamp(secs, timestamp, daysep, daytimesep, timesep, millissep) 394 return timestamp 395 396 # Seam for mocking 397 def _get_epoch(self): 398 return time.time() 399 400 def _use_cache(self, secs, *separators): 401 return self._previous_timestamp \ 402 and self._previous_secs == secs \ 403 and self._previous_separators == separators 404 405 def _cached_timestamp(self, millis, millissep): 406 if millissep: 407 return self._previous_timestamp + millissep + format(millis, '03d') 408 return self._previous_timestamp 409 410 def _cache_timestamp(self, secs, timestamp, daysep, daytimesep, timesep, millissep): 411 self._previous_secs = secs 412 self._previous_separators = (daysep, daytimesep, timesep) 413 self._previous_timestamp = timestamp[:-4] if millissep else timestamp 414 415 416TIMESTAMP_CACHE = TimestampCache() 417