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