Skip to content
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
186 lines (151 sloc) 5.58 KB
# bubblesub - ASS subtitle editor
# Copyright (C) 2018 Marcin Kurczewski
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <>.
"""Miscellaneous functions and classes for general purpose usage."""
import fractions
import itertools
import re
import typing as T
from pathlib import Path
def ms_to_times(milliseconds: int) -> T.Tuple[int, int, int, int]:
"""Convert PTS to tuple symbolizing time.
:param milliseconds: PTS
:return: tuple with hours, minutes, seconds and milliseconds
if milliseconds < 0:
milliseconds = 0
milliseconds = int(round(milliseconds))
hours, milliseconds = divmod(milliseconds, 3_600_000)
minutes, milliseconds = divmod(milliseconds, 60000)
seconds, milliseconds = divmod(milliseconds, 1000)
return hours, minutes, seconds, milliseconds
def ms_to_str(milliseconds: int) -> str:
"""Convert PTS to a human readable form.
:param milliseconds: PTS
:return: PTS representation in form of `[-]HH:MM:SS.mmm`
sgn = "-" if milliseconds < 0 else ""
hours, minutes, seconds, milliseconds = ms_to_times(abs(milliseconds))
return f"{sgn}{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}"
def str_to_ms(text: str) -> int:
"""Convert a human readable text in form of `[[-]HH:]MM:SS.mmm` to PTS.
:param text: input text
:return: PTS
result = re.match(
if result:
sign ="sign")
hour = int("hour"))
minute = int("minute"))
second = int("second"))
millisecond = int("millisecond"))
ret = ((((hour * 60) + minute) * 60) + second) * 1000 + millisecond
if sign == "-":
ret = -ret
return ret
raise ValueError(f'invalid time format: "{text}"')
def eval_expr(expr: str) -> T.Union[int, float, fractions.Fraction]:
"""Evaluate simple expression.
:param expr: expression to evaluate
:return: scalar result
import ast
import operator
op_map = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.BitXor: operator.xor,
ast.USub: operator.neg,
def _eval(
node: T.List[ast.stmt]
) -> T.Union[int, float, fractions.Fraction]:
if isinstance(node, ast.Num):
return fractions.Fraction(node.n)
if isinstance(node, ast.BinOp):
return op_map[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp):
return op_map[type(node.op)](_eval(node.operand))
raise TypeError(node)
return _eval(ast.parse(str(expr), mode="eval").body)
def make_ranges(
indexes: T.Iterable[int], reverse: bool = False
) -> T.Iterable[T.Tuple[int, int]]:
"""Group indexes together into a list of consecutive ranges.
:param indexes: list of source indexes
:param reverse: whether ranges should be made in reverse order
:return: list of tuples symbolizing start and end of each range
items = list(enumerate(sorted(indexes)))
if reverse:
for _, group in itertools.groupby(items, lambda item: item[1] - item[0]):
elems = list(group)
if reverse:
start_idx = elems[0][1]
end_idx = elems[-1][1]
yield (start_idx, end_idx + 1 - start_idx)
def sanitize_file_name(file_name: T.Union[Path, str]) -> str:
"""Remove unusable characters from a file name.
:param file_name: file name to sanitize
:return: sanitized file name
if isinstance(file_name, Path):
file_name = str(file_name.resolve())
file_name = file_name.replace("/", "_")
file_name = file_name.replace(":", ".")
file_name = file_name.replace(" ", "_")
file_name = re.sub(r"(?u)[^-\w.]", "", file_name)
return file_name
def chunks(source: T.List[T.Any], size: int) -> T.Iterable[T.List[T.Any]]:
"""Yield successive chunks of given size from source.
:param source: source list
:param size: chunk size
:return: chunks
for i in range(0, len(source), size):
yield source[i : i + size]
def first(source: T.Iterable[T.Any], default: T.Any = None) -> T.Any:
"""Return first element from a list or default value if the list is empty.
:param source: source list
:param default: default value
:return: first element or default value
return next(iter(source))
except StopIteration:
return default
def ucfirst(source: str) -> str:
"""Return source string with capitalized first letter.
:param source: source string
:return: transformed string
if not source:
return source
return source[0].upper() + source[1:]
You can’t perform that action at this time.