Source code for src.timeExpression

# SPDX-FileCopyrightText: Copyright © 2026 BBC
#
# SPDX-License-Identifier: BSD-3-Clause

import re

# Framerate multiplier e.g. "1000 1001"
_frm_regex = re.compile(r'(?P<numerator>\d+)\s(?P<denominator>\d+)')

# Clock-time Timecode with Frames e.g. "01:00:23:14"
_tc_regex = \
    re.compile(
        r'(?P<h>[0-9][0-9]):(?P<m>[0-5][0-9]):(?P<s>[0-5][0-9]):(?P<f>[0-9][0-9])$')

# Clock-time with no frames, hhmmss time e.g. "425000:13:14.040"
_hms_regex = \
    re.compile(
        r'(?P<h>[0-9][0-9]+):(?P<m>[0-5][0-9]):(?P<s>([0-5][0-9])(\.[0-9]+)?)$')

# Offset time e.g. "2016f"
_tm_regex = \
    re.compile(
        r'(?P<count>([0-9]+)(\.[0-9]+)?)(?P<metric>(h)|(m)|(s)|(ms)|(f)|(t))$')


[docs] class TimeExpressionHandler: _framerate = 25 _framerate_multiplier = 1.0 _effective_framerate = 25 _tickrate = 1 def _calculateEffectiveFramerate(self): return self._framerate * self._framerate_multiplier @classmethod def _decode_frm(cls, framerate_multiplier: str) -> float: m = _frm_regex.match(framerate_multiplier) if m is None: raise ValueError( 'Framerate multiplier {} not valid'.format( framerate_multiplier)) return int(m['numerator'])/int(m['denominator']) def __init__(self, framerate: str | None = None, framerate_multiplier: str | None = None, tickrate: str | None = None): if framerate is not None: self._framerate = int(framerate) if framerate_multiplier is not None: self._framerate_multiplier = \ TimeExpressionHandler._decode_frm(framerate_multiplier) self._effective_framerate = self._calculateEffectiveFramerate() if tickrate is not None: if int(tickrate) < 1: raise ValueError('tickrate must be positive integer') self._tickrate = int(tickrate) elif framerate is not None: self._tickrate = self._effective_framerate else: self._tickrate = 1
[docs] def seconds(self, time_value: str) -> float: # try hhmmss first m = _hms_regex.match(time_value) if m is not None: # print('Matched {} as mm:hh:ss.sss'.format(time_value)) seconds = \ int(m['h']) * 3600 + \ int(m['m']) * 60 + \ float(m['s']) return seconds # try offset time next m = _tm_regex.match(time_value) if m is not None: # print('Matched {} as offset time'.format(time_value)) count = float(m['count']) match m['metric']: case 'h': return count * 3600 case 'm': return count * 60 case 's': return count case 'ms': return count / 1000 case 'f': return count / self._effective_framerate case 't': return count / self._tickrate # finally try clock-time timecode m = _tc_regex.match(time_value) if m is not None: # print('Matched {} as mm:hh:ff'.format(time_value)) if int(m['f']) >= self._framerate: raise ValueError( '{} has illegal frame value for frame rate {}'.format( time_value, self._framerate )) seconds = \ int(m['h']) * 3600 + \ int(m['m']) * 60 + \ int(m['s']) + \ float(m['f']) / self._effective_framerate return seconds raise ValueError( '{} is not a recognised time expression'.format( time_value))
[docs] def isNonFrameClockTime(self, time_expression: str) -> bool: match = _hms_regex.match(time_expression) return match is not None
[docs] def isFrameClockTime(self, time_expression: str) -> bool: match = _tc_regex.match(time_expression) return match is not None
[docs] def isOffsetTime(self, time_expression: str) -> bool: match = _tm_regex.match(time_expression) return match is not None
[docs] def usesFrames(self, time_expression: str) -> bool: rv = self.isFrameClockTime(time_expression=time_expression) if not rv: # try Offset time m = _tm_regex.match(time_expression) if m is not None: rv = m['metric'] == 'f' return rv
[docs] def usesTicks(self, time_expression: str) -> bool: rv = False m = _tm_regex.match(time_expression) if m is not None: rv = m['metric'] == 't' return rv