Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Comment thread
WheheoHu marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support for timecodes whose hours field exceeds 99 hours. Colon-separated formats (SMPTE, SRT, FFMPEG, DLP) now accept an unbounded hours field (still zero-padded to at least 2 digits), so non-strict timecodes such as `100:00:00:00` are parsed correctly. This lifts the previous -99–99 hour range limit; single-digit unpadded hours (`1:00:00:00`) remain invalid

支持小时字段超过 99 小时的时码。冒号分隔格式(SMPTE、SRT、FFMPEG、DLP)现在接受无上限的小时字段(仍需补零至至少两位),因此 `100:00:00:00` 等非严格模式时码可被正确解析。此前 -99–99 小时的范围限制已被取消;未补零的单位数小时(`1:00:00:00`)仍视为无效

## [1.0.0] - 2026-05-12

### Summary
Expand Down
16 changes: 10 additions & 6 deletions dftt_timecode/pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
This module defines regex patterns for matching various professional timecode formats
used in film, television, and video production. All patterns support negative timecodes
with an optional leading minus sign.

For the colon-separated formats (SMPTE, SRT, FFMPEG, DLP), the hours field must be
zero-padded to at least 2 digits but is otherwise unbounded, so timecodes may exceed
99 hours (e.g. ``100:00:00:00``) as produced in non-strict mode without 24h wraparound.
"""

import re

SMPTE_NDF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d):){1}(\d?\d\d{1}){1}$')
SMPTE_NDF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d):){1}(\d?\d\d{1}){1}$')
"""Regex pattern for SMPTE Non-Drop-Frame (NDF) timecode format.

Matches: ``HH:MM:SS:FF`` where frames use colon separator.
Expand All @@ -21,7 +25,7 @@
The colon separator before frames distinguishes NDF from drop-frame format.
"""

SMPTE_DF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d);){1}(\d?\d\d{1}){1}$')
SMPTE_DF_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d);){1}(\d?\d\d{1}){1}$')
"""Regex pattern for SMPTE Drop-Frame (DF) timecode format.

Matches: ``HH:MM:SS;FF`` where frames use semicolon separator.
Expand All @@ -35,7 +39,7 @@
Used primarily with 29.97 fps and its multiples (59.94, 119.88).
"""

SMPTE_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d);?:?){1}(\d?\d\d{1}){1}$')
SMPTE_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d);?:?){1}(\d?\d\d{1}){1}$')
"""Regex pattern for any SMPTE timecode format (both NDF and DF).

Matches: ``HH:MM:SS:FF`` or ``HH:MM:SS;FF``
Expand All @@ -44,7 +48,7 @@
accepting either colon or semicolon before the frame number.
"""

SRT_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d),){1}(\d\d\d){1}$')
SRT_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d),){1}(\d\d\d){1}$')
"""Regex pattern for SubRip (SRT) subtitle timecode format.

Matches: ``HH:MM:SS,mmm`` where mmm is milliseconds.
Expand All @@ -56,7 +60,7 @@
Uses comma separator before milliseconds. This format is frame rate independent.
"""

FFMPEG_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d)\.){1}(\d?\d+){1}$')
FFMPEG_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d)\.){1}(\d?\d+){1}$')
"""Regex pattern for FFmpeg timecode format.

Matches: ``HH:MM:SS.ss`` where ss is sub-seconds (centiseconds).
Expand All @@ -68,7 +72,7 @@
Uses period separator before sub-seconds. Commonly used in video processing tools.
"""

DLP_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d\d{1}):){1}([0-5]?\d):){1}([0-5]?\d):){1}([01][0-9][0-9]|2[0-4][0-9]|25[0]){1}$')
DLP_REGEX = re.compile(r'^(?:-)?(?:(?:(?:(\d{2,}):){1}([0-5]?\d):){1}([0-5]?\d):){1}([01][0-9][0-9]|2[0-4][0-9]|25[0]){1}$')
"""Regex pattern for DLP Cinema timecode format.

Matches: ``HH:MM:SS:sss`` where sss is sub-frames (0-249).
Expand Down
5 changes: 5 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ __pycache__/
.DS_Store
Thumbs.db

# Compiled gettext catalogs — regenerated from .po at build time
# (sphinx-build auto-compiles with gettext_auto_build). Keep .po in git,
# not the binary .mo artifacts.
locale/**/*.mo

# Note: _static/ and _templates/ are NOT ignored
# because they contain source files for the documentation
# (custom templates and static assets like switcher.json)
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Features
- **Multiple Timecode Format Support**: SMPTE (DF/NDF), SRT, DLP (Cine Canvas), FFMPEG, FCPX, frame count, timestamp
- **High Frame Rate Support**: Supports frame rates from 0.01 to 999.99 fps
- **Drop-Frame/Non-Drop-Frame**: Strictly supports SMPTE DF/NDF formats
- **Extended Time Range**: Currently supports time range from -99 to 99 hours
- **Extended Time Range**: Hours are unbounded (zero-padded to at least 2 digits), so non-strict timecodes may exceed 99 hours (e.g. ``100:00:00:00``)
- **Strict Mode**: 24-hour cycling mode that automatically converts timecodes outside the 0-24 hour range
- **High Precision**: Internal storage using high-precision Fraction timestamps for accurate conversions
- **Rich Operators**: Comprehensive support for arithmetic and comparison operations between timecodes and numbers
Expand Down
Binary file removed docs/locale/zh_CN/LC_MESSAGES/README.mo
Binary file not shown.
Binary file removed docs/locale/zh_CN/LC_MESSAGES/api/dftt_timecode.mo
Binary file not shown.
Binary file removed docs/locale/zh_CN/LC_MESSAGES/api/dftt_timerange.mo
Binary file not shown.
Binary file removed docs/locale/zh_CN/LC_MESSAGES/api/error.mo
Binary file not shown.
Binary file removed docs/locale/zh_CN/LC_MESSAGES/changelog.mo
Binary file not shown.
Binary file removed docs/locale/zh_CN/LC_MESSAGES/contributing.mo
Binary file not shown.
Binary file removed docs/locale/zh_CN/LC_MESSAGES/index.mo
Binary file not shown.
9 changes: 6 additions & 3 deletions docs/locale/zh_CN/LC_MESSAGES/index.po
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,12 @@ msgstr "**跳帧/非跳帧**:严格支持 SMPTE DF/NDF 格式"

#: ../../index.rst:25
msgid ""
"**Extended Time Range**: Currently supports time range from -99 to 99 "
"hours"
msgstr "**扩展时间范围**:目前支持 -99 到 99 小时的时间范围"
"**Extended Time Range**: Hours are unbounded (zero-padded to at least 2 "
"digits), so non-strict timecodes may exceed 99 hours (e.g. "
"``100:00:00:00``)"
msgstr ""
"**扩展时间范围**:小时数无上限(需补零至至少 2 位),因此非严格模式下的时码可超过 99 "
"小时(例如 ``100:00:00:00``)"

#: ../../index.rst:26
msgid ""
Expand Down
Binary file removed docs/locale/zh_CN/LC_MESSAGES/installation.mo
Binary file not shown.
Binary file removed docs/locale/zh_CN/LC_MESSAGES/quickstart.mo
Binary file not shown.
Binary file removed docs/locale/zh_CN/LC_MESSAGES/user_guide.mo
Binary file not shown.
39 changes: 39 additions & 0 deletions test/test_dftt_timecode.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,45 @@ def test_invalid_timecode(timecode_value, timecode_type, fps, drop_frame, strict
TC(timecode_value, timecode_type, fps, drop_frame, strict)


@pytest.mark.parametrize(
"timecode_value, timecode_type, fps, drop_frame, strict, output_type, expected_output",
[
("100:00:00:00", "auto", 24, False, False, "smpte", "100:00:00:00"),
("123:45:59:12", "auto", 24, False, False, "smpte", "123:45:59:12"),
("1000:00:00:00", "auto", 24, False, False, "smpte", "1000:00:00:00"),
("100:00:00;00", "auto", 29.97, True, False, "smpte", "100:00:00;00"),
("100:00:00,000", "srt", 24, False, False, "srt", "100:00:00,000"),
("1000:00:00,000", "srt", 24, False, False, "srt", "1000:00:00,000"),
("100:00:00.67", "ffmpeg", 24, False, False, "ffmpeg", "100:00:00.67"),
("100:00:00:102", "dlp", 24, False, False, "dlp", "100:00:00:102"),
],
ids=["smpte_ndf", "smpte_ndf_full", "smpte_ndf_4digit", "smpte_df", "srt", "srt_4digit", "ffmpeg", "dlp"],
)
def test_hours_over_two_digits(
timecode_value, timecode_type, fps, drop_frame, strict, output_type, expected_output
):
# Non-strict timecodes may exceed 99h; the hours field must parse and round-trip.
tc = TC(timecode_value, timecode_type, fps, drop_frame, strict)
assert tc.timecode_output(output_type) == expected_output


def test_hours_over_two_digits_framecount():
assert TC("100:00:00:00", "auto", 24, False, False).framecount == 100 * 3600 * 24
assert TC("1000:00:00:00", "auto", 24, False, False).framecount == 1000 * 3600 * 24
assert (
TC("123:45:59:12", "auto", 24, False, False).framecount
== 123 * 3600 * 24 + 45 * 60 * 24 + 59 * 24 + 12
)


def test_unpadded_single_digit_hours_rejected():
# Hours must still be zero-padded to at least 2 digits; single-digit is invalid.
from dftt_timecode.error import DFTTTimecodeTypeError

with pytest.raises(DFTTTimecodeTypeError):
TC("1:00:00:00", "auto", 24, False, False)


@pytest.mark.parametrize(
"timecode_value, timecode_type, fps, drop_frame, strict, result_fps",
[
Expand Down
Loading