def from_range(cls, start, end, step=1): """ Build a :class:`FrameSet` from given start and end frames. Args: start (int): The first frame of the :class:`FrameSet`. end (int): The last frame of the :class:`FrameSet`. step (int, optional): Range step (default 1). Returns: :class:`FrameSet`: """ # match range() exception if not isinstance(step, int): raise TypeError("integer step argument expected, got {}." .format(type(step))) elif step == 0: raise ValueError("step argument must not be zero") elif step == 1: start, end = normalizeFrames([start, end]) range_str = "{0}-{1}".format(start, end) else: start, end = normalizeFrames([start, end]) step = normalizeFrame(step) range_str = "{0}-{1}x{2}".format(start, end, step) return FrameSet(range_str)
def _build_frange_part_decimal( start, stop, count, stride, min_stride, max_stride, zfill=0 ): """ Private method: builds a proper and padded subframe range string from decimal values. Args: start (decimal.Decimal): first frame stop (decimal.Decimal): last frame count (int): number of frames in range (inclusive) stride (decimal.Decimal or None): stride to use if known else None min_stride (decimal.Decimal): minimum increment that will produce correctly rounded frames max_stride (decimal.Decimal): maximum increment that will produce correctly rounded frames zfill (int): width for zero padding Returns: str: """ if stride is None: # Use an exact stride value if within allowed limits for # range, otherwise use midpoint of stride limits stride = (stop - start) / (count - 1) if not min_stride <= stride <= max_stride: stride = (min_stride + max_stride) / 2 # Minimise number of decimal places in stride stride_range = max_stride - min_stride stride_range = stride_range.as_tuple() leading_zeros = abs(len(stride_range.digits) + stride_range.exponent) stride = abs(quantize(stride, leading_zeros + 1)).normalize() # Adjust end frame if required so correct number of steps is # calculated when recreating FrameSet from frange string while abs(stop - start) / stride + 1 < count: exponent = stop.as_tuple().exponent delta = decimal.Decimal(1).scaleb(exponent) stop += delta.copy_sign(stop) start, stop = normalizeFrames([start, stop]) return FrameSet._build_frange_part(start, stop, stride, zfill=zfill)
def framesToFrameRanges(frames, zfill=0): """ Converts a sequence of frames to a series of padded frame range strings. Args: frames (collections.Iterable): sequence of frames to process zfill (int): width for zero padding Yields: str: """ _build = FrameSet._build_frange_part _build_decimal = FrameSet._build_frange_part_decimal # Ensure all frame values are of same type frames = normalizeFrames(frames) curr_start = None curr_stride = None curr_strides = None # used for decimal frame handling only curr_min_stride = None # used for decimal frame handling only curr_max_stride = None # used for decimal frame handling only curr_frame = None last_frame = None curr_count = 0 for curr_frame in frames: if curr_start is None: curr_start = curr_frame last_frame = curr_frame curr_count += 1 continue if curr_stride is None: curr_stride = abs(curr_frame-curr_start) curr_strides = {curr_stride} new_stride = abs(curr_frame-last_frame) # Handle decimal strides and frame rounding if isinstance(curr_stride, decimal.Decimal): # Check whether stride difference could be caused by rounding if len(curr_strides) == 1: stride_delta = abs(curr_stride - new_stride) exponent = stride_delta.as_tuple().exponent max_stride_delta = decimal.Decimal(1).scaleb(exponent) if stride_delta <= max_stride_delta: curr_strides.add(new_stride) if new_stride in curr_strides: # Find minimum frame value that rounds to current min_frame = (curr_frame - max_stride_delta / 2) while min_frame.quantize(curr_frame) != curr_frame: min_frame = min_frame.next_plus() # Find maximum frame value that rounds to current max_frame = (curr_frame + max_stride_delta / 2) while max_frame.quantize(curr_frame) != curr_frame: max_frame = max_frame.next_minus() # Adjust min stride limit until frame rounds to current while True: new_min_stride = (min_frame - curr_start) / curr_count test_frame = curr_start + new_min_stride * curr_count if test_frame.quantize(curr_frame) == curr_frame: break min_frame = min_frame.next_plus() # Adjust max stride limit until frame rounds to current while True: new_max_stride = (max_frame - curr_start) / curr_count test_frame = curr_start + new_max_stride * curr_count if test_frame.quantize(curr_frame) == curr_frame: break max_frame = max_frame.next_minus() # Update minimum and maximum stride values for overall range if curr_min_stride is not None: new_min_stride = max(curr_min_stride, new_min_stride) if curr_max_stride is not None: new_max_stride = min(curr_max_stride, new_max_stride) # A stride exists that rounds all frame values correctly if new_min_stride <= new_max_stride: new_stride = curr_stride curr_min_stride = new_min_stride curr_max_stride = new_max_stride if curr_stride == new_stride: curr_count += 1 elif curr_count == 2 and curr_stride != 1: yield _build(curr_start, curr_start, None, zfill) curr_start = last_frame curr_stride = new_stride curr_strides = {new_stride} curr_min_stride = None curr_max_stride = None else: if isinstance(curr_stride, decimal.Decimal): stride = curr_strides.pop() if len(curr_strides) == 1 else None yield _build_decimal(curr_start, last_frame, curr_count, stride, curr_min_stride, curr_max_stride, zfill) else: yield _build(curr_start, last_frame, curr_stride, zfill) curr_stride = None curr_strides = None curr_min_stride = None curr_max_stride = None curr_start = curr_frame curr_count = 1 last_frame = curr_frame if curr_count == 2 and curr_stride != 1: yield _build(curr_start, curr_start, None, zfill) yield _build(curr_frame, curr_frame, None, zfill) else: if isinstance(curr_stride, decimal.Decimal): stride = curr_strides.pop() if len(curr_strides) == 1 else None yield _build_decimal(curr_start, curr_frame, curr_count, stride, curr_min_stride, curr_max_stride, zfill) else: yield _build(curr_start, curr_frame, curr_stride, zfill)
def __init__(self, frange): """Initialize the :class:`FrameSet` object. """ # if the user provides anything but a string, short-circuit the build if not isinstance(frange, futils.string_types): # if it's apparently a FrameSet already, short-circuit the build if set(dir(frange)).issuperset(self.__slots__): for attr in self.__slots__: setattr(self, attr, getattr(frange, attr)) return # if it's inherently disordered, sort and build elif isinstance(frange, Set): self._maxSizeCheck(frange) self._items = frozenset(normalizeFrames(frange)) self._order = tuple(sorted(self._items)) self._frange = self.framesToFrameRange( self._order, sort=False, compress=False) return # if it's ordered, find unique and build elif isinstance(frange, Sequence): self._maxSizeCheck(frange) items = set() order = unique(items, normalizeFrames(frange)) self._order = tuple(order) self._items = frozenset(items) self._frange = self.framesToFrameRange( self._order, sort=False, compress=False) return # if it's an individual number build directly elif isinstance(frange, futils.integer_types + (float, decimal.Decimal)): frame = normalizeFrame(frange) self._order = (frame, ) self._items = frozenset([frame]) self._frange = self.framesToFrameRange( self._order, sort=False, compress=False) # in all other cases, cast to a string else: try: frange = asString(frange) except Exception as err: msg = 'Could not parse "{0}": cast to string raised: {1}' raise ParseException(msg.format(frange, err)) # we're willing to trim padding characters from consideration # this translation is orders of magnitude faster than prior method if futils.PY2: frange = bytes(frange).translate(None, ''.join(self.PAD_MAP.keys())) self._frange = asString(frange) else: frange = str(frange) for key in self.PAD_MAP: frange = frange.replace(key, '') self._frange = asString(frange) # because we're acting like a set, we need to support the empty set if not self._frange: self._items = frozenset() self._order = tuple() return # build the mutable stores, then cast to immutable for storage items = set() order = [] maxSize = constants.MAX_FRAME_SIZE frange_parts = [] frange_types = [] for part in self._frange.split(","): # this is to deal with leading / trailing commas if not part: continue # parse the partial range start, end, modifier, chunk = self._parse_frange_part(part) frange_parts.append((start, end, modifier, chunk)) frange_types.extend(map(type, (start, end, chunk))) # Determine best type for numbers in range. Note that # _parse_frange_part will always return decimal.Decimal for subframes FrameType = int if decimal.Decimal in frange_types: FrameType = decimal.Decimal for start, end, modifier, chunk in frange_parts: # handle batched frames (1-100x5) if modifier == 'x': frames = xfrange(start, end, chunk, maxSize=maxSize) frames = [FrameType(f) for f in frames if f not in items] self._maxSizeCheck(len(frames) + len(items)) order.extend(frames) items.update(frames) # handle staggered frames (1-100:5) elif modifier == ':': if '.' in futils.native_str(chunk): raise ValueError("Unable to stagger subframes") for stagger in range(chunk, 0, -1): frames = xfrange(start, end, stagger, maxSize=maxSize) frames = [f for f in frames if f not in items] self._maxSizeCheck(len(frames) + len(items)) order.extend(frames) items.update(frames) # handle filled frames (1-100y5) elif modifier == 'y': if '.' in futils.native_str(chunk): raise ValueError("Unable to fill subframes") not_good = frozenset(xfrange(start, end, chunk, maxSize=maxSize)) frames = xfrange(start, end, 1, maxSize=maxSize) frames = (f for f in frames if f not in not_good) frames = [f for f in frames if f not in items] self._maxSizeCheck(len(frames) + len(items)) order.extend(frames) items.update(frames) # handle full ranges and single frames else: frames = xfrange(start, end, 1 if start < end else -1, maxSize=maxSize) frames = [FrameType(f) for f in frames if f not in items] self._maxSizeCheck(len(frames) + len(items)) order.extend(frames) items.update(frames) # lock the results into immutable internals # this allows for hashing and fast equality checking self._items = frozenset(items) self._order = tuple(order)