use std::{
fmt::Display,
io::{self, BufRead, BufReader, BufWriter, Read, Write},
ops::Deref,
result,
sync::LazyLock,
};
use chrono::Duration;
use clap::ValueEnum;
use regex::Regex;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SrtError {
#[error("Parse time error: {0}")]
ParseTimeError(String),
#[error("Parse error occurred while read content: {0}")]
ParseTextError(String),
#[error("IO error")]
IoError(io::Error),
#[error("Failed to fix overlapping entries. The problematic line is {0}")]
OverlapError(String),
#[error("Generated an invalid timestamp.")]
InvalidTsError,
}
pub type Result<T> = result::Result<T, SrtError>;
#[derive(Debug)]
pub struct SrtFile {
entries: Vec<SubtitleEntry>,
}
#[derive(Clone, ValueEnum)]
pub enum OverlapFixMode {
#[clap(name = "1")]
Before,
#[clap(name = "2")]
After,
}
impl Deref for SrtFile {
type Target = Vec<SubtitleEntry>;
fn deref(&self) -> &Self::Target {
&self.entries
}
}
impl SrtFile {
pub fn read<R: Read>(r: R) -> Result<Self> {
let br = BufReader::new(r);
let mut lines = br
.lines()
.map(|line| line.map_err(|e| SrtError::IoError(e)));
let mut entries = Vec::new();
loop {
let index_line = match lines.next() {
Some(Ok(l)) => l,
Some(Err(e)) => return Err(e),
None => break,
};
let index_line = index_line.trim();
if index_line.is_empty() {
continue;
}
let index = index_line
.parse::<u32>()
.map_err(|_| SrtError::ParseTextError(index_line.to_string()))?;
let ts_line = lines
.next()
.ok_or_else(|| SrtError::ParseTextError("Missing timestamp line".to_string()))??;
let timestamp = SrtTime::from_line(&ts_line)?;
let mut text = String::new();
while let Some(Ok(l)) = lines.next() {
if l.trim().is_empty() {
break;
}
if !text.is_empty() {
#[cfg(target_family = "unix")]
text.push('\n');
#[cfg(target_family = "windows")]
text.push_str("\r\n");
}
text.push_str(&l);
}
entries.push(SubtitleEntry {
index,
timestamp,
text,
});
}
Ok(Self { entries })
}
pub fn write<W: Write>(&self, w: &mut W) -> Result<()> {
let mut bw = BufWriter::new(w);
for entry in &self.entries {
#[cfg(target_family = "unix")]
bw.write_fmt(format_args!(
"{}\n{}\n{}\n\n",
entry.index, entry.timestamp, entry.text
))
.map_err(|e| SrtError::IoError(e))?;
}
bw.flush().map_err(|e| SrtError::IoError(e))?;
Ok(())
}
pub fn check_ts_overlap(&self) -> bool {
match self.entries.len() {
0 | 1 => false,
_ => {
let mut start_point = self.entries[0].timestamp.end_ts;
for entry in self.entries.iter().skip(1) {
if entry.timestamp.beg_ts < start_point {
return true;
}
start_point = entry.timestamp.end_ts
}
false
}
}
}
pub fn fix_ts_overlap(&mut self, mode: OverlapFixMode) -> Result<()> {
if !self.check_ts_overlap() {
Ok(())
} else {
let mut plan: Vec<(usize, Duration, bool)> = Vec::new();
for i in 1..self.entries.len() {
let prev_entry = &self.entries[i - 1];
let curr_entry = &self.entries[i];
if curr_entry.timestamp.beg_ts < prev_entry.timestamp.end_ts {
match mode {
OverlapFixMode::Before => {
let new_beg_ts = prev_entry.timestamp.end_ts;
if new_beg_ts > curr_entry.timestamp.end_ts {
return Err(SrtError::OverlapError(format!(
"Fixing current entry would result in non-positive duration: prev({}) curr({})",
prev_entry.timestamp, curr_entry.timestamp
)));
}
plan.push((i, new_beg_ts, true));
}
OverlapFixMode::After => {
let new_end_ts = curr_entry.timestamp.beg_ts;
if new_end_ts < prev_entry.timestamp.beg_ts {
return Err(SrtError::OverlapError(format!(
"Fixing previous entry would result in non-positive duration: prev({}) curr({})",
prev_entry.timestamp, curr_entry.timestamp
)));
}
plan.push((i - 1, new_end_ts, false));
}
}
}
}
if plan.is_empty() {
return Ok(());
}
for (index, new_ts, is_beg) in plan {
let entry = &mut self.entries[index];
if is_beg {
entry.timestamp.update_beg_ts(new_ts);
} else {
entry.timestamp.update_end_ts(new_ts);
}
}
Ok(())
}
}
pub fn adjust_timestamps(&mut self, delta: i64, fix_mode: OverlapFixMode) -> Result<()> {
let is_add = delta.is_positive();
let delta = Duration::milliseconds(delta.abs());
for entry in &mut self.entries {
if !is_add && entry.timestamp.beg_ts >= delta {
entry.timestamp.beg_ts -= delta;
entry.timestamp.end_ts -= delta;
}
if is_add {
entry.timestamp.beg_ts += delta;
entry.timestamp.end_ts += delta;
}
if !entry.timestamp.is_valid() {
return Err(SrtError::InvalidTsError);
}
}
self.fix_ts_overlap(fix_mode)
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct SubtitleEntry {
pub index: u32,
timestamp: SrtTime,
pub text: String,
}
impl SubtitleEntry {
pub fn to_entry_str(&self) -> Vec<String> {
let mut res = vec![String::new(); 3];
res[0] = self.index.to_string();
res[1] = format!("{}", self.timestamp);
res[2] = self.text.clone();
res
}
}
static SRT_TIME_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})$").unwrap()
});
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct SrtTime {
beg_ts: Duration,
end_ts: Duration,
dur: Duration,
}
impl Display for SrtTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{} --> {}",
Self::dur_to_timestamp(self.beg_ts),
Self::dur_to_timestamp(self.end_ts)
))
}
}
impl SrtTime {
pub fn new(beg_ts: Duration, end_ts: Duration) -> Self {
Self {
beg_ts,
end_ts,
dur: end_ts - beg_ts,
}
}
pub fn entry_dur_secs(&self) -> i64 {
self.dur.num_seconds()
}
pub fn entry_dur_millis(&self) -> i64 {
self.dur.num_milliseconds()
}
fn update_beg_ts(&mut self, new_ts: Duration) {
self.beg_ts = new_ts;
self.dur = self.end_ts - self.beg_ts
}
fn update_end_ts(&mut self, new_ts: Duration) {
self.end_ts = new_ts;
self.dur = self.end_ts - self.beg_ts
}
fn is_valid(&self) -> bool {
let max = Duration::hours(100);
self.beg_ts < max && self.end_ts < max
}
fn dur_to_timestamp(dur: Duration) -> String {
let ms = dur.num_milliseconds();
let sig = if ms.is_negative() { "-" } else { "" };
let ms = ms.abs();
let s = ms / 1_000;
let ms = ms % 1_000;
let m = s / 60;
let s = s % 60;
let h = m / 60;
let m = m % 60;
return format!("{}{:02}:{:02}:{:02},{:03}", sig, h, m, s, ms);
}
fn from_line(ts_line: &str) -> Result<Self> {
if SRT_TIME_RE.is_match(ts_line) {
let mat = SRT_TIME_RE.captures(ts_line).unwrap();
let (beg_h, beg_m, beg_s, beg_ms) = (&mat[1], &mat[2], &mat[3], &mat[4]);
let beg_ts = Self::from_seg(beg_h, beg_m, beg_s, beg_ms)?;
let (end_h, end_m, end_s, end_ms) = (&mat[5], &mat[6], &mat[7], &mat[8]);
let end_ts = Self::from_seg(end_h, end_m, end_s, end_ms)?;
if end_ts < beg_ts {
return Err(SrtError::ParseTimeError(
"endtime is greater then begintime".to_string(),
));
}
Ok(Self {
beg_ts,
end_ts,
dur: end_ts - beg_ts,
})
} else {
Err(SrtError::ParseTimeError(ts_line.to_string()))
}
}
fn from_seg(h: &str, m: &str, s: &str, ms: &str) -> Result<Duration> {
let mut start = Duration::zero();
let h: i64 = h.parse().map_err(|_| {
SrtError::ParseTimeError(format!("invalid hour value {}", h.to_string()))
})?;
let m: i64 = m.parse().map_err(|_| {
SrtError::ParseTimeError(format!("invalid minute value {}", m.to_string()))
})?;
let s: i64 = s.parse().map_err(|_| {
SrtError::ParseTimeError(format!("invalid second value {}", s.to_string()))
})?;
let ms: i64 = ms.parse().map_err(|_| {
SrtError::ParseTimeError(format!("invalid millisecond value {}", ms.to_string()))
})?;
if h > 99 || h < 0 || m > 59 || m < 0 || s > 59 || s < 0 || ms > 999 || ms < 0 {
return Err(SrtError::ParseTimeError(format!(
"invalid timestamp: {:02}:{:02}:{:02}.{:03}",
h, m, s, ms
)));
}
start += Duration::hours(h)
+ Duration::minutes(m)
+ Duration::seconds(s)
+ Duration::milliseconds(ms);
Ok(start)
}
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Seek, SeekFrom};
use super::*;
use chrono::Duration;
fn create_entry(beg_s: i64, end_s: i64) -> SubtitleEntry {
SubtitleEntry {
index: 1,
timestamp: SrtTime::new(Duration::seconds(beg_s), Duration::seconds(end_s)),
text: "".to_string(),
}
}
#[test]
fn test_to_entry_str() {
let entry = create_entry(100, 200);
println!("entry is {:?}", entry.to_entry_str())
}
#[test]
fn test_adjust_timestamps_positive() {
let mut srt_file = SrtFile {
entries: vec![create_entry(10, 15), create_entry(20, 25)],
};
let result = srt_file.adjust_timestamps(3000, OverlapFixMode::Before);
assert!(result.is_ok());
let expected_entries = vec![create_entry(13, 18), create_entry(23, 28)];
assert_eq!(srt_file.entries, expected_entries);
}
#[test]
fn test_adjust_timestamps_negative() {
let mut srt_file = SrtFile {
entries: vec![create_entry(10, 15), create_entry(20, 25)],
};
let result = srt_file.adjust_timestamps(-5000, OverlapFixMode::Before);
assert!(result.is_ok());
let expected_entries = vec![create_entry(5, 10), create_entry(15, 20)];
assert_eq!(srt_file.entries, expected_entries);
}
#[test]
fn test_adjust_timestamps_negative_clamping() {
let mut srt_file = SrtFile {
entries: vec![create_entry(2, 5), create_entry(10, 15)],
};
let result = srt_file.adjust_timestamps(-5000, OverlapFixMode::Before);
assert!(result.is_ok());
let expected_entries = vec![
create_entry(2, 5), create_entry(5, 10),
];
assert_eq!(srt_file.entries, expected_entries);
}
#[test]
fn test_adjust_timestamps_overflow() {
let mut srt_file = SrtFile {
entries: vec![
create_entry(359999, 360000), ],
};
let result = srt_file.adjust_timestamps(1000, OverlapFixMode::Before);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SrtError::InvalidTsError));
}
#[test]
fn test_no_overlap_fix() {
let mut srt_file = SrtFile {
entries: vec![
create_entry(0, 5),
create_entry(5, 10),
create_entry(11, 15),
],
};
let original_entries = srt_file.entries.clone();
let result = srt_file.fix_ts_overlap(OverlapFixMode::After);
assert!(result.is_ok());
assert_eq!(srt_file.entries, original_entries);
}
#[test]
fn test_fix_after_mode() {
let mut srt_file = SrtFile {
entries: vec![create_entry(0, 5), create_entry(4, 10), create_entry(9, 15)],
};
let result = srt_file.fix_ts_overlap(OverlapFixMode::After);
assert!(result.is_ok());
let expected_entries = vec![
create_entry(0, 4), create_entry(4, 9), create_entry(9, 15),
];
assert_eq!(srt_file.entries, expected_entries);
}
#[test]
fn test_fix_before_mode() {
let mut srt_file = SrtFile {
entries: vec![create_entry(0, 5), create_entry(4, 10), create_entry(9, 15)],
};
let result = srt_file.fix_ts_overlap(OverlapFixMode::Before);
assert!(result.is_ok());
let expected_entries = vec![
create_entry(0, 5),
create_entry(5, 10), create_entry(10, 15), ];
assert_eq!(srt_file.entries, expected_entries);
}
#[test]
fn test_zero_duration_is_valid() {
let mut srt_file = SrtFile {
entries: vec![create_entry(5, 10), create_entry(5, 10)],
};
let result = srt_file.fix_ts_overlap(OverlapFixMode::After);
assert!(result.is_ok());
let expected_entries = vec![create_entry(5, 5), create_entry(5, 10)];
assert_eq!(srt_file.entries, expected_entries);
let mut srt_file = SrtFile {
entries: vec![create_entry(5, 10), create_entry(5, 10)],
};
let result = srt_file.fix_ts_overlap(OverlapFixMode::Before);
assert!(result.is_ok());
let expected_entries = vec![create_entry(5, 10), create_entry(10, 10)];
assert_eq!(srt_file.entries, expected_entries);
}
#[test]
fn test_negative_duration_is_invalid() {
let mut srt_file = SrtFile {
entries: vec![create_entry(10, 15), create_entry(5, 20)],
};
let original_entries = srt_file.entries.clone();
let result = srt_file.fix_ts_overlap(OverlapFixMode::After);
assert!(result.is_err());
assert_eq!(srt_file.entries, original_entries);
}
#[test]
fn test_no_overlap() {
let srt_file = SrtFile {
entries: vec![
create_entry(0, 5),
create_entry(6, 10),
create_entry(11, 15),
],
};
assert!(!srt_file.check_ts_overlap());
}
#[test]
fn test_touching_entries() {
let srt_file = SrtFile {
entries: vec![
create_entry(0, 5),
create_entry(5, 10),
create_entry(10, 15),
],
};
assert!(!srt_file.check_ts_overlap());
}
#[test]
fn test_with_overlap() {
let srt_file = SrtFile {
entries: vec![
create_entry(0, 5),
create_entry(4, 10), create_entry(11, 15),
],
};
assert!(srt_file.check_ts_overlap());
}
#[test]
fn test_overlap_in_middle() {
let srt_file = SrtFile {
entries: vec![
create_entry(0, 5),
create_entry(6, 10),
create_entry(9, 15), ],
};
assert!(srt_file.check_ts_overlap());
}
#[test]
fn test_contained_entry() {
let srt_file = SrtFile {
entries: vec![
create_entry(0, 10),
create_entry(2, 8), ],
};
assert!(srt_file.check_ts_overlap());
}
#[test]
fn test_empty_file_overlap() {
let srt_file = SrtFile { entries: vec![] };
assert!(!srt_file.check_ts_overlap());
}
#[test]
fn test_single_entry() {
let srt_file = SrtFile {
entries: vec![create_entry(0, 5)],
};
assert!(!srt_file.check_ts_overlap());
}
#[test]
fn test_complex_overlap() {
let srt_file = SrtFile {
entries: vec![
create_entry(0, 5),
create_entry(5, 10),
create_entry(12, 17),
create_entry(16, 20), ],
};
assert!(srt_file.check_ts_overlap());
}
#[test]
fn test_write_single_entry() {
let entry = SubtitleEntry {
index: 1,
timestamp: SrtTime::new(Duration::seconds(1), Duration::seconds(2)),
text: "Hello world!".to_string(),
};
let srt_file = SrtFile {
entries: vec![entry],
};
let mut output = Cursor::new(Vec::new());
srt_file.write(&mut output).unwrap();
output.seek(SeekFrom::Start(0)).unwrap();
let mut output_str = String::new();
output.read_to_string(&mut output_str).unwrap();
let expected = "1\n00:00:01,000 --> 00:00:02,000\nHello world!\n\n";
assert_eq!(&output_str, expected);
}
#[test]
fn test_write_multiple_entries() {
let entry1 = SubtitleEntry {
index: 1,
timestamp: SrtTime::new(Duration::seconds(1), Duration::seconds(2)),
text: "Hello.".to_string(),
};
let entry2 = SubtitleEntry {
index: 2,
timestamp: SrtTime::new(Duration::seconds(3), Duration::seconds(4)),
text: "World.".to_string(),
};
let srt_file = SrtFile {
entries: vec![entry1, entry2],
};
let mut output = Cursor::new(Vec::new());
srt_file
.write(&mut output)
.expect("Failed to write the srt entries");
let expected = "1\n00:00:01,000 --> 00:00:02,000\nHello.\n\n2\n00:00:03,000 --> 00:00:04,000\nWorld.\n\n";
output.seek(SeekFrom::Start(0)).unwrap();
let mut output_str = String::new();
output.read_to_string(&mut output_str).unwrap();
assert_eq!(&output_str, expected);
}
#[test]
fn test_standard_duration() {
let dur = Duration::hours(1)
+ Duration::minutes(15)
+ Duration::seconds(30)
+ Duration::milliseconds(500);
assert_eq!(SrtTime::dur_to_timestamp(dur), "01:15:30,500");
}
#[test]
fn test_just_minutes() {
let dur = Duration::minutes(12);
assert_eq!(SrtTime::dur_to_timestamp(dur), "00:12:00,000");
}
#[test]
fn test_just_milliseconds() {
let dur = Duration::milliseconds(999);
assert_eq!(SrtTime::dur_to_timestamp(dur), "00:00:00,999");
}
#[test]
fn test_zero_duration() {
let dur = Duration::zero();
assert_eq!(SrtTime::dur_to_timestamp(dur), "00:00:00,000");
}
#[test]
fn test_max_srt_duration() {
let dur = Duration::hours(99)
+ Duration::minutes(59)
+ Duration::seconds(59)
+ Duration::milliseconds(999);
assert_eq!(SrtTime::dur_to_timestamp(dur), "99:59:59,999");
}
#[test]
fn test_overflow_to_minutes() {
let dur = Duration::seconds(59) + Duration::milliseconds(1000);
assert_eq!(SrtTime::dur_to_timestamp(dur), "00:01:00,000");
}
#[test]
fn test_overflow_to_hours() {
let dur = Duration::minutes(59) + Duration::seconds(59) + Duration::milliseconds(1000);
assert_eq!(SrtTime::dur_to_timestamp(dur), "01:00:00,000");
}
#[test]
fn test_large_duration_beyond_srt_limit() {
let dur = Duration::hours(101);
assert_eq!(SrtTime::dur_to_timestamp(dur), "101:00:00,000");
}
#[test]
fn test_negative_duration() {
let dur = Duration::hours(-1);
assert_eq!(SrtTime::dur_to_timestamp(dur), "-01:00:00,000");
}
fn read_from_str(s: &str) -> Result<SrtFile> {
SrtFile::read(Cursor::new(s))
}
#[test]
fn test_valid_single_entry() {
let srt_content = "1\n00:00:01,000 --> 00:00:02,000\nHello world!\n\n";
let srt_file = read_from_str(srt_content).unwrap();
assert_eq!(srt_file.entries.len(), 1);
let entry = &srt_file.entries[0];
assert_eq!(entry.index, 1);
assert_eq!(entry.timestamp.beg_ts, Duration::seconds(1));
assert_eq!(entry.timestamp.end_ts, Duration::seconds(2));
assert_eq!(entry.text, "Hello world!");
}
#[test]
fn test_valid_multiple_entries() {
let srt_content = "1\n00:00:01,000 --> 00:00:02,000\nHello.\n\n2\n00:00:03,000 --> 00:00:04,000\nWorld.\n\n";
let srt_file = read_from_str(srt_content).unwrap();
assert_eq!(srt_file.entries.len(), 2);
assert_eq!(srt_file.entries[0].text, "Hello.");
assert_eq!(srt_file.entries[1].text, "World.");
}
#[test]
fn test_multiline_text() {
let srt_content = "1\n00:00:01,000 --> 00:00:02,000\nHello\nworld!\n\n";
let srt_file = read_from_str(srt_content).unwrap();
assert_eq!(srt_file.entries.len(), 1);
assert_eq!(srt_file.entries[0].text, "Hello\nworld!");
}
#[test]
fn test_file_without_trailing_empty_line() {
let srt_content = "1\n00:00:01,000 --> 00:00:02,000\nHello world!";
let srt_file = read_from_str(srt_content).unwrap();
assert_eq!(srt_file.entries.len(), 1);
assert_eq!(srt_file.entries[0].text, "Hello world!");
}
#[test]
fn test_invalid_index() {
let srt_content = "one\n00:00:01,000 --> 00:00:02,000\nHello.";
let err = read_from_str(srt_content).unwrap_err();
assert!(matches!(err, SrtError::ParseTextError(_)));
}
#[test]
fn test_invalid_timestamp_format() {
let srt_content = "1\n00:00:01,000 - 00:00:02,000\nHello."; let err = read_from_str(srt_content).unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
}
#[test]
fn test_empty_file() {
let srt_content = "";
let srt_file = read_from_str(srt_content).unwrap();
assert_eq!(srt_file.entries.len(), 0);
}
#[test]
fn test_only_empty_lines() {
let srt_content = "\n\n\n";
let srt_file = read_from_str(srt_content).unwrap();
assert_eq!(srt_file.entries.len(), 0);
}
#[test]
fn test_valid_timestamp_line() {
let line = "00:01:10,250 --> 00:01:15,500";
let result = SrtTime::from_line(line).unwrap();
let expected_beg =
Duration::minutes(1) + Duration::seconds(10) + Duration::milliseconds(250);
let expected_end =
Duration::minutes(1) + Duration::seconds(15) + Duration::milliseconds(500);
assert_eq!(result.beg_ts, expected_beg);
assert_eq!(result.end_ts, expected_end);
}
#[test]
fn test_boundary_timestamps() {
let line = "00:00:00,000 --> 99:59:59,999";
let result = SrtTime::from_line(line).unwrap();
let expected_beg = Duration::zero();
let expected_end = Duration::hours(99)
+ Duration::minutes(59)
+ Duration::seconds(59)
+ Duration::milliseconds(999);
assert_eq!(result.beg_ts, expected_beg);
assert_eq!(result.end_ts, expected_end);
let line = "01:00:00,000 --> 01:00:00,000";
let result = SrtTime::from_line(line).unwrap();
let expected_beg_end = Duration::hours(1);
assert_eq!(result.beg_ts, expected_beg_end);
assert_eq!(result.end_ts, expected_beg_end);
}
#[test]
fn test_invalid_line_format() {
let line = "00:01:10,250 00:01:15,500";
let err = SrtTime::from_line(line).unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains(line));
let line = "00:01:10,250 --> 00:01:15";
let err = SrtTime::from_line(line).unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains(line));
let line = "00:01:15,500 --> 00:01:10,250";
let err = SrtTime::from_line(line).unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains("endtime is greater then begintime"));
}
#[test]
fn test_valid_time_components() {
let result = SrtTime::from_seg("01", "15", "30", "500").unwrap();
let expected = Duration::hours(1)
+ Duration::minutes(15)
+ Duration::seconds(30)
+ Duration::milliseconds(500);
assert_eq!(result, expected);
let result = SrtTime::from_seg("00", "00", "00", "123").unwrap();
let expected = Duration::milliseconds(123);
assert_eq!(result, expected);
let result = SrtTime::from_seg("00", "00", "00", "000").unwrap();
let expected = Duration::zero();
assert_eq!(result, expected);
let result = SrtTime::from_seg("12", "00", "00", "000").unwrap();
let expected = Duration::hours(12);
assert_eq!(result, expected);
}
#[test]
fn test_boundary_values() {
let result = SrtTime::from_seg("99", "59", "59", "999").unwrap();
let expected = Duration::hours(99)
+ Duration::minutes(59)
+ Duration::seconds(59)
+ Duration::milliseconds(999);
assert_eq!(result, expected);
let result = SrtTime::from_seg("01", "02", "03", "000").unwrap();
let expected = Duration::hours(1) + Duration::minutes(2) + Duration::seconds(3);
assert_eq!(result, expected);
let result = SrtTime::from_seg("00", "00", "00", "000").unwrap();
assert_eq!(result, Duration::zero());
}
#[test]
fn test_invalid_input_and_parsing_errors() {
let err = SrtTime::from_seg("100", "00", "00", "000").unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains("invalid timestamp"));
let err = SrtTime::from_seg("00", "60", "00", "000").unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains("invalid timestamp"));
let err = SrtTime::from_seg("00", "00", "60", "000").unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains("invalid timestamp"));
let err = SrtTime::from_seg("00", "00", "00", "1000").unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains("invalid timestamp"));
let err = SrtTime::from_seg("a", "00", "00", "000").unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains("invalid hour"));
let err = SrtTime::from_seg("-1", "00", "00", "000").unwrap_err();
assert!(matches!(err, SrtError::ParseTimeError(_)));
assert!(format!("{}", err).contains("invalid timestamp"));
}
}