|
|
|
@ -5,7 +5,7 @@ extern crate lazy_static; |
|
|
|
|
|
|
|
|
|
use regex::Regex; |
|
|
|
|
use std::fs::{File, OpenOptions}; |
|
|
|
|
use std::io::{BufReader, Write}; |
|
|
|
|
use std::io::{BufReader, Write as ioWrite, StdoutLock, StdinLock}; |
|
|
|
|
use std::io; |
|
|
|
|
use std::str::FromStr; |
|
|
|
|
use std::convert::TryFrom; |
|
|
|
@ -13,6 +13,7 @@ use std::io::BufRead; |
|
|
|
|
use std::ops::{Add, Mul, MulAssign, AddAssign, SubAssign, Sub}; |
|
|
|
|
use std::fmt::{self, Display}; |
|
|
|
|
use serde::export::fmt::Debug; |
|
|
|
|
use core::fmt::Write as fmtWrite; |
|
|
|
|
|
|
|
|
|
const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"]; |
|
|
|
|
const SPAMMY_LIBS: [&str; 5] = ["tokio_reactor", "hyper", "reqwest", "mio", "want"]; |
|
|
|
@ -21,6 +22,31 @@ fn main() { |
|
|
|
|
let argv = |
|
|
|
|
clap::App::new("srtune") |
|
|
|
|
.version(env!("CARGO_PKG_VERSION")) |
|
|
|
|
.about("\ |
|
|
|
|
'srtune' helps you edit a .srt file to match a video. Input and output can be either a file, or a stream, \ |
|
|
|
|
so you can pipe multiple invocations to create more complex operations. However, a single \ |
|
|
|
|
invocation should suffice in most cases.\n\ |
|
|
|
|
\n\ |
|
|
|
|
Times are specified with colons (required) and always include seconds (HH:MM:SS, MM:SS, 0:SS, :SS). \ |
|
|
|
|
Decimal point in the seconds part, if needed, can be either a period or a comma; times can be copied from the \ |
|
|
|
|
.srt file. Numbers without colons are assumed to be subtitle indices.\n\ |
|
|
|
|
\n\ |
|
|
|
|
The tool can be used iteratively, adjusting the invocation until the generated \ |
|
|
|
|
subtitle file matches the audio track. You can reload the file in VLC by dragging it \ |
|
|
|
|
onto the player window. To make this work, subtitle times specified in arguments are the \ |
|
|
|
|
ones seen in the output file (after shifts and moving), while entry indices are those from the input file.\n\ |
|
|
|
|
\n\ |
|
|
|
|
Using indices makes it easier to specify a subtitle to alter, but it is tied to the one .srt file. \ |
|
|
|
|
Times are harder to write, but the one configuration will work for any locatization or variant of the file,\ |
|
|
|
|
so long as it is intended for the same version of the movie. Enable debug logging with '-v' to see times \ |
|
|
|
|
you can use in place of indices. |
|
|
|
|
\n\ |
|
|
|
|
Indices are normally not renumbered, so the output file can be used as a reference \ |
|
|
|
|
for both times and indices when you work out the right set of arguments. The flag '--renumber' \ |
|
|
|
|
will give each output entry a new sequential number. Please note that, once renumbered, \ |
|
|
|
|
the indices in the output file should no longer be used in the command invocation, \ |
|
|
|
|
as there can be (and often will be) differences from the original file.\ |
|
|
|
|
") |
|
|
|
|
.arg(clap::Arg::with_name("input") |
|
|
|
|
.value_name("INFILE") |
|
|
|
|
.help("Input file, leave out for stdin"), |
|
|
|
@ -31,44 +57,46 @@ fn main() { |
|
|
|
|
.value_name("OUTFILE") |
|
|
|
|
.help("Output file, defaults to stdout"), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("fromtime") |
|
|
|
|
.short("f") |
|
|
|
|
.long("from-time") |
|
|
|
|
.value_name("TIME") |
|
|
|
|
.help("Time of the first affected entry, defaults to 0. Use seconds, M:S or \ |
|
|
|
|
H:M:S, decimals are supported. Uses the original times before any shifts or \ |
|
|
|
|
scaling."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("fromindex") |
|
|
|
|
.short("F") |
|
|
|
|
.long("from-index") |
|
|
|
|
.value_name("INDEX") |
|
|
|
|
.help("Time of the first affected entry, defaults to 0. Use seconds, M:S or \ |
|
|
|
|
H:M:S, decimals are supported. Uses the original times before any shifts or \ |
|
|
|
|
scaling."), |
|
|
|
|
) |
|
|
|
|
// .arg(clap::Arg::with_name("drop")
|
|
|
|
|
// .short("d")
|
|
|
|
|
// .long("drop")
|
|
|
|
|
// .value_name("ITEMS")
|
|
|
|
|
// .help("Drop one or multiple entries (separated by comma). Entries can be TIME \
|
|
|
|
|
// or INDEX. Two entries can be joined by '..' to specify a range. Use '..ENTRY'\
|
|
|
|
|
// or 'ENTRY..' to drop all in one direction. Ranges are inclusive."),
|
|
|
|
|
// )
|
|
|
|
|
.arg(clap::Arg::with_name("move") |
|
|
|
|
.short("m") |
|
|
|
|
.long("move") |
|
|
|
|
.value_name("TIME") |
|
|
|
|
.help("Move subtitles in time. Use seconds, M:S or H:M:S, decimals and minus \ |
|
|
|
|
are supported."), |
|
|
|
|
.value_name("OFFSET") |
|
|
|
|
.help("Move all subtitles in time (e.g 12:00.15 or -0:44)"), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("automove") |
|
|
|
|
.short("M") |
|
|
|
|
.long("automove") |
|
|
|
|
.value_name("ENTRY=VIDEOTIME") |
|
|
|
|
.multiple(true) |
|
|
|
|
.help("Move subtitles starting at a given time or index to align with \ |
|
|
|
|
a matching audio track time. This argument can be given multiple times. \ |
|
|
|
|
Some subtitles may be dropped if they fall outside the timeline after \ |
|
|
|
|
the move."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("scale") |
|
|
|
|
.short("s") |
|
|
|
|
.long("scale") |
|
|
|
|
.value_name("RATIO") |
|
|
|
|
.help("Scale subtitle times and durations to compensate for bitrate \ |
|
|
|
|
differences. 1 means identity, 1.1 makes all times 10% longer. If a start \ |
|
|
|
|
time was given, the scaling is relative to this point, otherwise to the first \ |
|
|
|
|
subtitle in the file. Has no effect if '--autoscale' is used."), |
|
|
|
|
.help("Scale all subtitle times and durations to compensate for bitrate \ |
|
|
|
|
differences. 1 means identity, 1.1 makes all times 10% longer. Scaling is \ |
|
|
|
|
relative to the first emitted subtitle; align it with '--move'. This option \ |
|
|
|
|
has no effect if '--autoscale' is used."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("autoscale") |
|
|
|
|
.short("S") |
|
|
|
|
.long("autoscale") |
|
|
|
|
.value_name("SUBTIME=VIDEOTIME") |
|
|
|
|
.help("Calculate scaling based on a perceived difference. The scaling is \ |
|
|
|
|
related to the first subtitle, so ensure it is aligned properly with '--move'."), |
|
|
|
|
related to the first emitted subtitle; align it with '--move'. \ |
|
|
|
|
This overrides '--scale'."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("durscale") |
|
|
|
|
.short("d") |
|
|
|
@ -81,10 +109,10 @@ fn main() { |
|
|
|
|
.arg(clap::Arg::with_name("renumber") |
|
|
|
|
.short("r") |
|
|
|
|
.long("renumber") |
|
|
|
|
.help("Change all numbers to be sequential starting with 1"), |
|
|
|
|
.help("Renumber all emitted entries with sequential 1-based numbers."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("v").short("v").multiple(true).help( |
|
|
|
|
"Sets the level of verbosity (adds to the default - info)", |
|
|
|
|
"Increase the logging verbosity; can be used multiple times", |
|
|
|
|
)) |
|
|
|
|
.get_matches(); |
|
|
|
|
|
|
|
|
@ -117,39 +145,25 @@ fn main() { |
|
|
|
|
} |
|
|
|
|
builder.init(); |
|
|
|
|
|
|
|
|
|
let from_time = match argv.value_of("fromtime") { |
|
|
|
|
Some(s) => { |
|
|
|
|
SubDuration::try_from(s).expect("Bad --from-time format").as_instant() |
|
|
|
|
} |
|
|
|
|
None => SubInstant(0f32) |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let from_index = match argv.value_of("fromindex") { |
|
|
|
|
Some(s) => { |
|
|
|
|
s.parse().expect("Bad --from-index format") |
|
|
|
|
} |
|
|
|
|
None => 0u32 |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let shift = match argv.value_of("move") { |
|
|
|
|
Some(s) => { |
|
|
|
|
SubDuration::try_from(s).expect("Bad --move format") |
|
|
|
|
} |
|
|
|
|
None => SubDuration(0f32) |
|
|
|
|
None => SubDuration(0f64) |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let scale = match argv.value_of("scale") { |
|
|
|
|
Some(s) => { |
|
|
|
|
s.parse().expect("Bad --scale format") |
|
|
|
|
} |
|
|
|
|
None => 1f32 |
|
|
|
|
None => 1f64 |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let durscale = match argv.value_of("durscale") { |
|
|
|
|
Some(s) => { |
|
|
|
|
s.parse().expect("Bad --durscale format") |
|
|
|
|
} |
|
|
|
|
None => 1f32 |
|
|
|
|
None => 1f64 |
|
|
|
|
}; // always also shrink durations
|
|
|
|
|
|
|
|
|
|
let autoscale = match argv.value_of("autoscale") { |
|
|
|
@ -159,32 +173,79 @@ fn main() { |
|
|
|
|
panic!("Bad --autoscale format, should be SUBTIME=VIDEOTIME") |
|
|
|
|
} |
|
|
|
|
let (first, second) = (halves[0], halves[1]); |
|
|
|
|
let subtime = SubDuration::try_from(first).expect("Bad --autoscale format").as_instant(); |
|
|
|
|
let vidtime = SubDuration::try_from(second).expect("Bad --autoscale format").as_instant(); |
|
|
|
|
if !first.contains(':') || !second.contains(':') { |
|
|
|
|
panic!("'--autoscale' requires two times"); |
|
|
|
|
} |
|
|
|
|
let subtime = SubDuration::try_from(first).expect("Bad --autoscale time format").as_instant(); |
|
|
|
|
let vidtime = SubDuration::try_from(second).expect("Bad --autoscale time format").as_instant(); |
|
|
|
|
|
|
|
|
|
Some((subtime, vidtime)) |
|
|
|
|
} |
|
|
|
|
None => None |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let mut automove = Vec::<AutoMoveTag>::new(); |
|
|
|
|
let mut automove_indices = vec![]; |
|
|
|
|
match argv.values_of("automove") { |
|
|
|
|
Some(ss) => { |
|
|
|
|
for s in ss { |
|
|
|
|
let halves: Vec<&str> = s.split("=").collect(); |
|
|
|
|
if halves.len() != 2 { |
|
|
|
|
panic!("Bad --automove format, should be ENTRY=VIDEOTIME") |
|
|
|
|
} |
|
|
|
|
let (first, second) = (halves[0], halves[1]); |
|
|
|
|
if !second.contains(':') { |
|
|
|
|
panic!("'--automove' requires time after '='"); |
|
|
|
|
} |
|
|
|
|
let vidtime = SubDuration::try_from(second).expect("Bad --automove format").as_instant(); |
|
|
|
|
|
|
|
|
|
if first.contains(':') { |
|
|
|
|
let subtime = SubDuration::try_from(first).expect("Bad --automove format").as_instant(); |
|
|
|
|
automove.push(AutoMoveTag::ByTime(subtime, vidtime)); |
|
|
|
|
} else { |
|
|
|
|
let index : u32 = first.parse().expect("Bad --automove format"); |
|
|
|
|
|
|
|
|
|
if automove_indices.contains(&index) { |
|
|
|
|
panic!("Index {} already used in automove.", index); |
|
|
|
|
} |
|
|
|
|
automove_indices.push(index); |
|
|
|
|
|
|
|
|
|
automove.push(AutoMoveTag::ByIndex(index, vidtime)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
None => (/* no automoves */) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let inf = argv.value_of("input"); |
|
|
|
|
let outf = argv.value_of("output"); |
|
|
|
|
let stdin = io::stdin(); |
|
|
|
|
let stdout = io::stdout(); |
|
|
|
|
|
|
|
|
|
let mut lines_iterator: Box<dyn Iterator<Item=Result<String, io::Error>>> = match inf { |
|
|
|
|
let renumber = argv.is_present("renumber"); |
|
|
|
|
|
|
|
|
|
let opts = TransformOpts { |
|
|
|
|
renumber, |
|
|
|
|
autoscale, |
|
|
|
|
durscale, |
|
|
|
|
scale, |
|
|
|
|
shift, |
|
|
|
|
automove, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let lines_iterator: Box<dyn SubsInput> = match inf { |
|
|
|
|
None => { |
|
|
|
|
Box::new(stdin.lock().lines()) |
|
|
|
|
Box::new(StdinSubsInput::new(stdin.lock())) |
|
|
|
|
} |
|
|
|
|
Some(f) => { |
|
|
|
|
let file = File::open(f).expect(&format!("Could not open file: {:?}", f)); |
|
|
|
|
Box::new(BufReader::new(file).lines()) |
|
|
|
|
Box::new(FileSubsInput::new(file)) |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let mut outfile: Box<dyn Write> = match outf { |
|
|
|
|
let outfile: Box<dyn SubsOutput> = match outf { |
|
|
|
|
None => { |
|
|
|
|
Box::new(stdout.lock()) |
|
|
|
|
Box::new(StdoutSubsOutput::new(stdout.lock())) |
|
|
|
|
} |
|
|
|
|
Some(f) => { |
|
|
|
|
let file = OpenOptions::new() |
|
|
|
@ -193,45 +254,132 @@ fn main() { |
|
|
|
|
.write(true) |
|
|
|
|
.open(f) |
|
|
|
|
.expect(&format!("Could not open file: {:?}", f)); |
|
|
|
|
Box::new(file) |
|
|
|
|
Box::new(FileSubsOutput::new(file)) |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let renumber = argv.is_present("renumber"); |
|
|
|
|
transform_subtitles(lines_iterator, outfile, opts); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
transform_subtitles(&mut lines_iterator, &mut outfile, TransformOpts { |
|
|
|
|
renumber, |
|
|
|
|
autoscale, |
|
|
|
|
durscale, |
|
|
|
|
scale, |
|
|
|
|
shift, |
|
|
|
|
from_index, |
|
|
|
|
from_time, |
|
|
|
|
}); |
|
|
|
|
//region SubInput
|
|
|
|
|
trait SubsInput : Iterator<Item=String> {} |
|
|
|
|
|
|
|
|
|
struct StdinSubsInput<'a> { |
|
|
|
|
inner : io::Lines<StdinLock<'a>> |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl<'a> Iterator for StdinSubsInput<'a> { |
|
|
|
|
type Item = String; |
|
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> { |
|
|
|
|
self.inner.next().map_or(None, Result::ok) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl<'a> StdinSubsInput<'a> { |
|
|
|
|
pub fn new(lock : StdinLock<'a>) -> Self { |
|
|
|
|
Self { |
|
|
|
|
inner : lock.lines() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl<'a> SubsInput for StdinSubsInput<'a> {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct FileSubsInput { |
|
|
|
|
inner : io::Lines<BufReader<File>> |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl FileSubsInput { |
|
|
|
|
pub fn new(file : File) -> Self { |
|
|
|
|
Self { |
|
|
|
|
inner : BufReader::new(file).lines() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl Iterator for FileSubsInput { |
|
|
|
|
type Item = String; |
|
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> { |
|
|
|
|
self.inner.next().map_or(None, Result::ok) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl SubsInput for FileSubsInput {} |
|
|
|
|
//endregion
|
|
|
|
|
|
|
|
|
|
//region SubOutput
|
|
|
|
|
trait SubsOutput { |
|
|
|
|
fn emit(&mut self, subtitle : Subtitle); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct StdoutSubsOutput<'a> { |
|
|
|
|
inner : StdoutLock<'a> |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl<'a> StdoutSubsOutput<'a> { |
|
|
|
|
pub fn new(inner : StdoutLock<'a>) -> Self { |
|
|
|
|
Self { inner } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl<'a> SubsOutput for StdoutSubsOutput<'a> { |
|
|
|
|
fn emit(&mut self, subtitle: Subtitle) { |
|
|
|
|
self.inner.write(subtitle.to_string().as_bytes()).expect("failed to write"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct FileSubsOutput { |
|
|
|
|
inner : File |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl FileSubsOutput { |
|
|
|
|
pub fn new(inner : File) -> Self { |
|
|
|
|
Self { inner } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl SubsOutput for FileSubsOutput { |
|
|
|
|
fn emit(&mut self, subtitle: Subtitle) { |
|
|
|
|
self.inner.write(subtitle.to_string().as_bytes()).expect("failed to write"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
//endregion
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)] |
|
|
|
|
struct TransformOpts { |
|
|
|
|
renumber: bool, |
|
|
|
|
autoscale: Option<(SubInstant, SubInstant)>, |
|
|
|
|
durscale: f32, |
|
|
|
|
scale: f32, |
|
|
|
|
durscale: f64, |
|
|
|
|
scale: f64, |
|
|
|
|
shift: SubDuration, |
|
|
|
|
from_index: u32, |
|
|
|
|
from_time: SubInstant, |
|
|
|
|
automove: Vec<AutoMoveTag>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Debug)] |
|
|
|
|
enum AutoMoveTag { |
|
|
|
|
ByTime(SubInstant, SubInstant), |
|
|
|
|
ByIndex(u32, SubInstant) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Debug,Default,Clone)] |
|
|
|
|
struct IterState { |
|
|
|
|
start_time : Option<SubInstant>, |
|
|
|
|
renumber_i : u32, |
|
|
|
|
timeline_head : SubInstant, |
|
|
|
|
/// Queue last item for duration clipping
|
|
|
|
|
queued : Option<Subtitle>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io::Error>> + 'a>, |
|
|
|
|
outfile : &mut Box<dyn Write + 'a>, |
|
|
|
|
mut opts : TransformOpts) { |
|
|
|
|
fn transform_subtitles<'a>(mut lines : Box<dyn SubsInput + 'a>, mut outfile : Box<dyn SubsOutput + 'a>, mut opts : TransformOpts) { |
|
|
|
|
debug!("Opts: {:#?}", opts); |
|
|
|
|
|
|
|
|
|
let mut start_time = SubInstant(0f32); |
|
|
|
|
let mut first_found = false; |
|
|
|
|
let mut renumber_i: u32 = 0; |
|
|
|
|
let mut istate = IterState::default(); |
|
|
|
|
|
|
|
|
|
let mut linebuf = vec![]; |
|
|
|
|
while let Some(Ok(x)) = lines.next() { |
|
|
|
|
let mut linebuf : Vec<String> = vec![]; |
|
|
|
|
'lines: while let Some(x) = lines.next() { |
|
|
|
|
let mut x = x.trim(); |
|
|
|
|
if x.starts_with('\u{feff}') { |
|
|
|
|
debug!("Stripping BOM mark"); |
|
|
|
@ -249,75 +397,126 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io: |
|
|
|
|
match u32::from_str(x) { |
|
|
|
|
Ok(num) => { |
|
|
|
|
// println!("Entry {}", num);
|
|
|
|
|
let datesrow = lines.next().unwrap().unwrap(); |
|
|
|
|
let datesrow = lines.next().unwrap(); |
|
|
|
|
if datesrow.contains(" --> ") { |
|
|
|
|
let mut halves = datesrow.split(" --> "); |
|
|
|
|
let (first, second) = (halves.next().unwrap(), halves.next().unwrap()); |
|
|
|
|
let start = SubInstant::try_from(first).unwrap(); |
|
|
|
|
let end = SubInstant::try_from(second).unwrap(); |
|
|
|
|
let sub_start = SubInstant::try_from(first).unwrap(); |
|
|
|
|
let sub_end = SubInstant::try_from(second).unwrap(); |
|
|
|
|
|
|
|
|
|
linebuf.clear(); |
|
|
|
|
while let Some(Ok(x)) = lines.next() { |
|
|
|
|
'text: while let Some(x) = lines.next() { |
|
|
|
|
if x.is_empty() { |
|
|
|
|
break; // space between the entries
|
|
|
|
|
break 'text; // space between the entries
|
|
|
|
|
} |
|
|
|
|
linebuf.push(x); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let mut subtitle = Subtitle { |
|
|
|
|
num, |
|
|
|
|
start, |
|
|
|
|
dur: SubDuration(end.0 - start.0), |
|
|
|
|
start: sub_start, |
|
|
|
|
dur: sub_end - sub_start, |
|
|
|
|
text: linebuf.join("\n"), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if start >= opts.from_time && num >= opts.from_index { |
|
|
|
|
if !first_found { |
|
|
|
|
debug!("Scaling anchored at {} (#{}), editing starts", start, num); |
|
|
|
|
debug!("Shifting by: {}", opts.shift); |
|
|
|
|
if istate.start_time.is_none() { |
|
|
|
|
debug!("Scaling anchored at {} (#{})", sub_start, num); |
|
|
|
|
istate.start_time = Some(sub_start); |
|
|
|
|
|
|
|
|
|
if let Some((mut subt, mut vidt)) = opts.autoscale { |
|
|
|
|
debug!("Autoscale: VT {} -> ST {}", vidt, subt); |
|
|
|
|
subt -= sub_start; |
|
|
|
|
vidt -= sub_start + opts.shift; |
|
|
|
|
if subt.0 <= 0f64 { |
|
|
|
|
panic!("Error in autoscale, start time is negative or zero."); |
|
|
|
|
} |
|
|
|
|
if vidt.0 <= 0f64 { |
|
|
|
|
panic!("Error in autoscale, end time is negative or zero."); |
|
|
|
|
} |
|
|
|
|
debug!(" relative to #{}, after \"move\": VT {} -> ST {}", num, vidt, subt); |
|
|
|
|
opts.scale = vidt.0 / subt.0; |
|
|
|
|
debug!("Resolved scale as {}", opts.scale); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
opts.durscale *= opts.scale; |
|
|
|
|
debug!("Duration scaling is {}", opts.durscale); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
start_time = start; |
|
|
|
|
first_found = true; |
|
|
|
|
if opts.scale != 1f64 { |
|
|
|
|
let scaled = subtitle.start.scale(istate.start_time.unwrap(), opts.scale); |
|
|
|
|
trace!("Scale #{} ({}) -> {}", num, subtitle.start, scaled); |
|
|
|
|
subtitle.start = scaled; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let Some((mut subt, mut vidt)) = opts.autoscale { |
|
|
|
|
debug!("Autoscale: VT {} -> ST {}", vidt, subt); |
|
|
|
|
subt -= start_time; |
|
|
|
|
vidt -= start_time + opts.shift; |
|
|
|
|
if subt.0 <= 0f32 { |
|
|
|
|
panic!("Error in autoscale, start time is negative or zero."); |
|
|
|
|
subtitle.dur *= opts.durscale; |
|
|
|
|
|
|
|
|
|
let would_be_shifted_start = subtitle.start + opts.shift; |
|
|
|
|
|
|
|
|
|
let mut to_drop = vec![]; |
|
|
|
|
for (i, amove) in opts.automove.iter().enumerate() { |
|
|
|
|
match amove { |
|
|
|
|
AutoMoveTag::ByIndex(idx, ref vidt) => { |
|
|
|
|
if num >= *idx { |
|
|
|
|
let dif = *vidt - would_be_shifted_start; |
|
|
|
|
debug!("Move by index #{} starts at #{} ({}), diff {}, -M{}={}", *idx, num, subtitle.start, dif, would_be_shifted_start, *vidt); |
|
|
|
|
opts.shift += dif; |
|
|
|
|
to_drop.push(i); |
|
|
|
|
} else if would_be_shifted_start > *vidt { |
|
|
|
|
warn!("Discarding out-of-order entry #{} @ {} (timeline head is {})", num, subtitle.start, istate.timeline_head); |
|
|
|
|
continue 'lines; |
|
|
|
|
} |
|
|
|
|
if vidt.0 <= 0f32 { |
|
|
|
|
panic!("Error in autoscale, end time is negative or zero."); |
|
|
|
|
} |
|
|
|
|
AutoMoveTag::ByTime(ref subt, ref vidt) => { |
|
|
|
|
if would_be_shifted_start >= (*subt - 0.1f64) { // margin
|
|
|
|
|
let dif = *vidt - *subt; |
|
|
|
|
debug!("Move by time {} starts at #{} ({}), diff {}, -M{}={}", subt, num, subtitle.start, dif, subt, *vidt); |
|
|
|
|
opts.shift += dif; |
|
|
|
|
to_drop.push(i); |
|
|
|
|
} |
|
|
|
|
debug!(" relative to #{}, after \"move\": VT {} -> ST {}", num, vidt, subt); |
|
|
|
|
opts.scale = vidt.0 / subt.0; |
|
|
|
|
debug!("Resolved scale as {}", opts.scale); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
opts.durscale *= opts.scale; |
|
|
|
|
debug!("Duration scaling is {}", opts.durscale); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
for i in &to_drop { |
|
|
|
|
let x = opts.automove.swap_remove(*i); |
|
|
|
|
trace!("Clean up: {:?}", x); |
|
|
|
|
} |
|
|
|
|
if !to_drop.is_empty() { |
|
|
|
|
debug!("New shift: {}, Timeline head: {}", opts.shift, istate.timeline_head); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if opts.scale != 1f32 { |
|
|
|
|
subtitle.start = subtitle.start.scale(start_time, opts.scale); |
|
|
|
|
} |
|
|
|
|
if opts.shift.0 != 0f64 { |
|
|
|
|
let shifted = subtitle.start + opts.shift; |
|
|
|
|
trace!("Shift #{} ({}) by {} -> {}", num, subtitle.start, opts.shift, shifted); |
|
|
|
|
subtitle.start = shifted; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
subtitle.dur *= opts.durscale; |
|
|
|
|
subtitle.start += opts.shift; |
|
|
|
|
if subtitle.start <= istate.timeline_head { |
|
|
|
|
warn!("Discarding out-of-order entry #{} @ {} (timeline head is {})", num, subtitle.start, istate.timeline_head); |
|
|
|
|
continue 'lines; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if subtitle.start.0 < 0f32 { |
|
|
|
|
warn!("Discarding negative time entry #{} @ {:.3}s", subtitle.num, subtitle.start.0); |
|
|
|
|
continue; |
|
|
|
|
if subtitle.start.is_negative() { |
|
|
|
|
warn!("Discarding negative time entry #{} @ {:.3}s", num, sub_start); |
|
|
|
|
continue 'lines; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// advance numbering only for the really emitted entries
|
|
|
|
|
istate.timeline_head = subtitle.start; |
|
|
|
|
|
|
|
|
|
if opts.renumber { |
|
|
|
|
renumber_i += 1; |
|
|
|
|
subtitle.num = renumber_i; |
|
|
|
|
istate.renumber_i += 1; |
|
|
|
|
subtitle.num = istate.renumber_i; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
outfile.write(subtitle.to_string().as_bytes()).expect("failed to write"); |
|
|
|
|
if let Some(mut q) = istate.queued.take() { |
|
|
|
|
if q.start + q.dur > subtitle.start { |
|
|
|
|
let clipped = subtitle.start - q.start; |
|
|
|
|
debug!("Clipping duration of #{} ({}) to avoid overlap: {} -> {}", q.num, q.start, q.dur, clipped); |
|
|
|
|
q.dur = clipped; |
|
|
|
|
} |
|
|
|
|
outfile.emit(q); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
istate.queued = Some(subtitle); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
Err(e) => { |
|
|
|
@ -330,11 +529,15 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io: |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
outfile.flush().unwrap(); |
|
|
|
|
// emit the last entry
|
|
|
|
|
if let Some(q) = istate.queued.take() { |
|
|
|
|
outfile.emit(q); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, PartialEq, PartialOrd)] |
|
|
|
|
struct SubInstant(f32); |
|
|
|
|
//region Time types
|
|
|
|
|
#[derive(Default, Copy, Clone, PartialEq, PartialOrd)] |
|
|
|
|
struct SubInstant(f64); |
|
|
|
|
|
|
|
|
|
impl Debug for SubInstant { |
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { |
|
|
|
@ -342,8 +545,14 @@ impl Debug for SubInstant { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, PartialEq, PartialOrd)] |
|
|
|
|
struct SubDuration(f32); |
|
|
|
|
impl SubInstant { |
|
|
|
|
fn is_negative(&self) -> bool { |
|
|
|
|
self.0 < 0f64 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Default, Copy, Clone, PartialEq, PartialOrd)] |
|
|
|
|
struct SubDuration(f64); |
|
|
|
|
|
|
|
|
|
impl Debug for SubDuration { |
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { |
|
|
|
@ -353,7 +562,28 @@ impl Debug for SubDuration { |
|
|
|
|
|
|
|
|
|
impl Display for SubDuration { |
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { |
|
|
|
|
write!(f, "{}", SubInstant(self.0)) |
|
|
|
|
let sign = self.0.signum(); |
|
|
|
|
let mut secs = self.0.abs(); |
|
|
|
|
let hours = (secs / 3600f64).floor(); |
|
|
|
|
secs -= hours * 3600f64; |
|
|
|
|
let mins = (secs / 60f64).floor(); |
|
|
|
|
secs -= mins * 60f64; |
|
|
|
|
let msecs = ((secs % 1f64) * 1000f64).round(); |
|
|
|
|
secs = secs.floor(); |
|
|
|
|
|
|
|
|
|
if sign.is_sign_negative() { |
|
|
|
|
f.write_char('-')?; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if hours > 0f64 { |
|
|
|
|
write!(f, "{:02}:", hours)?; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if hours > 0f64 || mins > 0f64 { |
|
|
|
|
write!(f, "{:02}:{:02},{:03}", mins, secs, msecs) |
|
|
|
|
} else { |
|
|
|
|
write!(f, "{},{:03}", secs, msecs) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -373,35 +603,51 @@ impl Sub<SubDuration> for SubInstant { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl Mul<f32> for SubDuration { |
|
|
|
|
impl Sub<SubInstant> for SubInstant { |
|
|
|
|
type Output = SubDuration; |
|
|
|
|
|
|
|
|
|
fn sub(self, rhs: SubInstant) -> Self::Output { |
|
|
|
|
SubDuration(self.0 - rhs.0) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl Sub<f64> for SubInstant { |
|
|
|
|
type Output = SubInstant; |
|
|
|
|
|
|
|
|
|
fn sub(self, rhs: f64) -> Self::Output { |
|
|
|
|
SubInstant(self.0 - rhs) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl Mul<f64> for SubDuration { |
|
|
|
|
type Output = SubDuration; |
|
|
|
|
|
|
|
|
|
fn mul(self, rhs: f32) -> Self::Output { |
|
|
|
|
fn mul(self, rhs: f64) -> Self::Output { |
|
|
|
|
SubDuration(self.0 * rhs) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl MulAssign<f32> for SubDuration { |
|
|
|
|
fn mul_assign(&mut self, rhs: f32) { |
|
|
|
|
impl MulAssign<f64> for SubDuration { |
|
|
|
|
fn mul_assign(&mut self, rhs: f64) { |
|
|
|
|
self.0 *= rhs; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl AddAssign<f32> for SubDuration { |
|
|
|
|
fn add_assign(&mut self, rhs: f32) { |
|
|
|
|
impl AddAssign<f64> for SubDuration { |
|
|
|
|
fn add_assign(&mut self, rhs: f64) { |
|
|
|
|
self.0 += rhs; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl SubInstant { |
|
|
|
|
/// Scale by a factor with a custom start time
|
|
|
|
|
pub fn scale(&self, start: SubInstant, factor: f32) -> SubInstant { |
|
|
|
|
pub fn scale(&self, start: SubInstant, factor: f64) -> SubInstant { |
|
|
|
|
SubInstant(start.0 + (self.0 - start.0) * factor) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl AddAssign<f32> for SubInstant { |
|
|
|
|
fn add_assign(&mut self, rhs: f32) { |
|
|
|
|
impl AddAssign<f64> for SubInstant { |
|
|
|
|
fn add_assign(&mut self, rhs: f64) { |
|
|
|
|
self.0 += rhs; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -424,6 +670,18 @@ impl SubAssign<SubDuration> for SubInstant { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl SubAssign<SubDuration> for SubDuration { |
|
|
|
|
fn sub_assign(&mut self, rhs: SubDuration) { |
|
|
|
|
self.0 -= rhs.0; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl AddAssign<SubDuration> for SubDuration { |
|
|
|
|
fn add_assign(&mut self, rhs: SubDuration) { |
|
|
|
|
self.0 += rhs.0; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl AddAssign<SubDuration> for SubInstant { |
|
|
|
|
fn add_assign(&mut self, rhs: SubDuration) { |
|
|
|
|
self.0 += rhs.0; |
|
|
|
@ -432,19 +690,19 @@ impl AddAssign<SubDuration> for SubInstant { |
|
|
|
|
|
|
|
|
|
impl Display for SubInstant { |
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { |
|
|
|
|
// TODO optimize this
|
|
|
|
|
let sign = self.0.signum(); |
|
|
|
|
let mut secs = self.0.abs(); |
|
|
|
|
let hours = (secs / 3600f32).floor(); |
|
|
|
|
secs -= hours * 3600f32; |
|
|
|
|
let minutes = (secs / 60f32).floor(); |
|
|
|
|
secs -= minutes * 60f32; |
|
|
|
|
let msecs = ((secs % 1f32) * 1000f32).round(); |
|
|
|
|
let hours = (secs / 3600f64).floor(); |
|
|
|
|
secs -= hours * 3600f64; |
|
|
|
|
let minutes = (secs / 60f64).floor(); |
|
|
|
|
secs -= minutes * 60f64; |
|
|
|
|
let msecs = ((secs % 1f64) * 1000f64).round(); |
|
|
|
|
write!(f, "{}{:02}:{:02}:{:02},{:03}", |
|
|
|
|
if sign.is_sign_negative() { "-" } else { "" }, |
|
|
|
|
hours, minutes, secs.floor(), msecs) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
//endregion
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)] |
|
|
|
|
struct Subtitle { |
|
|
|
@ -475,15 +733,15 @@ impl TryFrom<&str> for SubInstant { |
|
|
|
|
|
|
|
|
|
match DATE_RE.captures(value) { |
|
|
|
|
Some(caps) => { |
|
|
|
|
let minus = if caps.get(1).is_some() { -1f32 } else { 1f32 }; |
|
|
|
|
let minus = if caps.get(1).is_some() { -1f64 } else { 1f64 }; |
|
|
|
|
|
|
|
|
|
let h = &caps["h"]; |
|
|
|
|
let m = &caps["m"]; |
|
|
|
|
let s = caps["s"].replace(",", "."); |
|
|
|
|
|
|
|
|
|
Ok(SubInstant(minus * (f32::from_str(h).unwrap() * 3600f32 + |
|
|
|
|
f32::from_str(m).unwrap() * 60f32 + |
|
|
|
|
f32::from_str(&s).unwrap()))) |
|
|
|
|
Ok(SubInstant(minus * (f64::from_str(h).unwrap() * 3600f64 + |
|
|
|
|
f64::from_str(m).unwrap() * 60f64 + |
|
|
|
|
f64::from_str(&s).unwrap()))) |
|
|
|
|
} |
|
|
|
|
None => Err(failure::format_err!("Error parsing time: {}", value)) |
|
|
|
|
} |
|
|
|
@ -499,21 +757,29 @@ impl SubDuration { |
|
|
|
|
impl TryFrom<&str> for SubDuration { |
|
|
|
|
type Error = failure::Error; |
|
|
|
|
|
|
|
|
|
fn try_from(value: &str) -> Result<Self, Self::Error> { |
|
|
|
|
fn try_from(mut value: &str) -> Result<Self, Self::Error> { |
|
|
|
|
lazy_static! { |
|
|
|
|
static ref TIME_RE: Regex = Regex::new(r"^(?U)(?P<n>-)?(?:(?P<h>\d+):)?(?:(?P<m>\d+):)?(?P<s>\d+(?:[.,]\d+)?)$").unwrap(); |
|
|
|
|
static ref TIME_RE: Regex = Regex::new(r"^(?U)(?:(?P<h>\d+):)?(?:(?P<m>\d+):)?(?P<s>\d+(?:[.,]\d+)?)$").unwrap(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let negative = value.starts_with('-'); |
|
|
|
|
if negative { |
|
|
|
|
value = &value[1..]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if value.starts_with(':') { |
|
|
|
|
// prefixed colon when someone is lazy to type 0:
|
|
|
|
|
value = &value[1..]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
match TIME_RE.captures(value) { |
|
|
|
|
Some(caps) => { |
|
|
|
|
let minus = if caps.name("n").is_some() { -1f32 } else { 1f32 }; |
|
|
|
|
let h = caps.name("h").map_or(0f32, |m| f32::from_str(m.as_str()).unwrap()); |
|
|
|
|
let m = caps.name("m").map_or(0f32, |m| f32::from_str(m.as_str()).unwrap()); |
|
|
|
|
let s = caps.name("s").map_or(0f32, |m| f32::from_str(&m.as_str().replace(",", ".")).unwrap()); |
|
|
|
|
|
|
|
|
|
Ok(SubDuration(minus * (h * 3600f32 + |
|
|
|
|
m * 60f32 + |
|
|
|
|
s))) |
|
|
|
|
let minus = if negative { -1f64 } else { 1f64 }; |
|
|
|
|
let h = caps.name("h").map_or(0f64, |m| f64::from_str(m.as_str()).unwrap()); |
|
|
|
|
let m = caps.name("m").map_or(0f64, |m| f64::from_str(m.as_str()).unwrap()); |
|
|
|
|
let s = caps.name("s").map_or(0f64, |m| f64::from_str(&m.as_str().replace(",", ".")).unwrap()); |
|
|
|
|
|
|
|
|
|
Ok(SubDuration(minus * (h * 3600f64 + m * 60f64 + s))) |
|
|
|
|
} |
|
|
|
|
None => { |
|
|
|
|
Err(failure::format_err!("Error parsing time: {}", value)) |
|
|
|
@ -525,38 +791,57 @@ impl TryFrom<&str> for SubDuration { |
|
|
|
|
#[test] |
|
|
|
|
fn test_parse_duration() { |
|
|
|
|
// this is used for user input on the command line
|
|
|
|
|
let bad = SubDuration(-1f32); |
|
|
|
|
assert_eq!(SubDuration(45678f32), SubDuration::try_from("45678").unwrap_or(bad), "integer secs"); |
|
|
|
|
assert_eq!(SubDuration(1.23f32), SubDuration::try_from("1.23").unwrap_or(bad), "float secs with period"); |
|
|
|
|
assert_eq!(SubDuration(-1.23f32), SubDuration::try_from("-1.23").unwrap_or(bad), "MINUS float secs with period"); |
|
|
|
|
assert_eq!(SubDuration(1.23f32), SubDuration::try_from("1,23").unwrap_or(bad), "float secs with comma"); |
|
|
|
|
assert_eq!(SubDuration(121.15f32), SubDuration::try_from("2:1.15").unwrap_or(bad), "m:s.frac"); |
|
|
|
|
assert_eq!(SubDuration(121.15f32), SubDuration::try_from("2:01.15").unwrap_or(bad), "m:0s.frac"); |
|
|
|
|
assert_eq!(SubDuration(121.15f32), SubDuration::try_from("02:01.15").unwrap_or(bad), "0m:0s.frac"); |
|
|
|
|
assert_eq!(SubDuration(121.15f32), SubDuration::try_from("02:01,15").unwrap_or(bad), "0m:0s,frac"); |
|
|
|
|
|
|
|
|
|
assert_eq!(SubDuration(3721.15f32), SubDuration::try_from("1:02:01,15").unwrap_or(bad), "h:0m:0s,frac"); |
|
|
|
|
assert_eq!(SubDuration(3721.15f32), SubDuration::try_from("1:02:01,15").unwrap_or(bad), "h:0m:0s.frac"); |
|
|
|
|
assert_eq!(SubDuration(3721.15f32), SubDuration::try_from("01:02:01,15").unwrap_or(bad), "0h:0m:0s,frac"); |
|
|
|
|
assert_eq!(SubDuration(-3721.15f32), SubDuration::try_from("-01:02:01,15").unwrap_or(bad), "-0h:0m:0s,frac"); |
|
|
|
|
let bad = SubDuration(-1f64); |
|
|
|
|
assert_eq!(SubDuration::try_from(":45678").unwrap_or(bad), SubDuration(45678f64), "integer secs with colon prefix"); |
|
|
|
|
assert_eq!(SubDuration::try_from("-:45678").unwrap_or(bad), SubDuration(-45678f64), "neg integer secs with colon prefix"); |
|
|
|
|
assert_eq!(SubDuration::try_from("45678").unwrap_or(bad), SubDuration(45678f64), "integer secs"); |
|
|
|
|
assert_eq!(SubDuration::try_from("1.23").unwrap_or(bad), SubDuration(1.23f64), "float secs with period"); |
|
|
|
|
assert_eq!(SubDuration::try_from("-1.23").unwrap_or(bad), SubDuration(-1.23f64), "MINUS float secs with period"); |
|
|
|
|
assert_eq!(SubDuration::try_from("1,23").unwrap_or(bad), SubDuration(1.23f64), "float secs with comma"); |
|
|
|
|
assert_eq!(SubDuration::try_from("2:1.15").unwrap_or(bad), SubDuration(121.15f64), "m:s.frac"); |
|
|
|
|
assert_eq!(SubDuration::try_from("2:01.15").unwrap_or(bad), SubDuration(121.15f64), "m:0s.frac"); |
|
|
|
|
assert_eq!(SubDuration::try_from("02:01.15").unwrap_or(bad), SubDuration(121.15f64), "0m:0s.frac"); |
|
|
|
|
assert_eq!(SubDuration::try_from("02:01,15").unwrap_or(bad), SubDuration(121.15f64), "0m:0s,frac"); |
|
|
|
|
|
|
|
|
|
assert_eq!(SubDuration::try_from("1:02:01,15").unwrap_or(bad), SubDuration(3721.15f64), "h:0m:0s,frac"); |
|
|
|
|
assert_eq!(SubDuration::try_from("1:02:01,15").unwrap_or(bad), SubDuration(3721.15f64), "h:0m:0s.frac"); |
|
|
|
|
assert_eq!(SubDuration::try_from("01:02:01,15").unwrap_or(bad), SubDuration(3721.15f64), "0h:0m:0s,frac"); |
|
|
|
|
assert_eq!(SubDuration::try_from("-01:02:01,15").unwrap_or(bad), SubDuration(-3721.15f64), "-0h:0m:0s,frac"); |
|
|
|
|
|
|
|
|
|
assert_eq!(SubDuration::try_from("18:01,755").unwrap_or(bad), SubDuration(1081.755f64)); |
|
|
|
|
assert_eq!(SubDuration::try_from("-18:01,755").unwrap_or(bad), SubDuration(-1081.755f64)); |
|
|
|
|
assert_eq!(SubDuration::try_from("-18:01.7").unwrap_or(bad), SubDuration(-1081.7f64)); |
|
|
|
|
assert_eq!(SubDuration::try_from("-12:18:01.7").unwrap_or(bad), SubDuration(-44281.7f64)); |
|
|
|
|
assert_eq!(SubDuration::try_from("0,000").unwrap_or(bad), SubDuration(0f64)); |
|
|
|
|
assert_eq!(SubDuration::try_from("-0,000").unwrap_or(bad), SubDuration(-0f64)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn test_parse_instant() { |
|
|
|
|
let bad = SubInstant(-1f32); |
|
|
|
|
assert_eq!(SubInstant(1081.755f32), SubInstant::try_from("00:18:01,755").unwrap_or(bad)); |
|
|
|
|
assert_eq!(SubInstant(1081.755f32), SubInstant::try_from("00:18:01.755").unwrap_or(bad)); |
|
|
|
|
assert_eq!(SubInstant(1081.7f32), SubInstant::try_from("00:18:01.7").unwrap_or(bad)); |
|
|
|
|
assert_eq!(SubInstant(1081.7f32), SubInstant::try_from("0:18:1.7").unwrap_or(bad)); |
|
|
|
|
assert_eq!(SubInstant(0f32), SubInstant::try_from("00:00:00,000").unwrap_or(bad)); |
|
|
|
|
assert_eq!(SubInstant(-3600f32), SubInstant::try_from("-01:00:00,000").unwrap_or(bad)); |
|
|
|
|
let bad = SubInstant(-1f64); |
|
|
|
|
assert_eq!(SubInstant::try_from("00:18:01,755").unwrap_or(bad), SubInstant(1081.755f64)); |
|
|
|
|
assert_eq!(SubInstant::try_from("00:18:01.755").unwrap_or(bad), SubInstant(1081.755f64)); |
|
|
|
|
assert_eq!(SubInstant::try_from("00:18:01.7").unwrap_or(bad), SubInstant(1081.7f64)); |
|
|
|
|
assert_eq!(SubInstant::try_from("0:18:1.7").unwrap_or(bad), SubInstant(1081.7f64)); |
|
|
|
|
assert_eq!(SubInstant::try_from("00:00:00,000").unwrap_or(bad), SubInstant(0f64)); |
|
|
|
|
assert_eq!(SubInstant::try_from("-01:00:00,000").unwrap_or(bad), SubInstant(-3600f64)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn test_stringify_instant() { |
|
|
|
|
assert_eq!("00:18:01,755", SubInstant::try_from("00:18:01,755").unwrap().to_string()); |
|
|
|
|
assert_eq!("-00:18:01,755", SubInstant::try_from("-00:18:01,755").unwrap().to_string()); |
|
|
|
|
assert_eq!("-00:18:01,700", SubInstant::try_from("-00:18:01.7").unwrap().to_string()); |
|
|
|
|
assert_eq!("00:00:00,000", SubInstant::try_from("00:00:00,000").unwrap().to_string()); |
|
|
|
|
assert_eq!("-00:00:00,000", SubInstant::try_from("-00:00:00,000").unwrap().to_string()); |
|
|
|
|
assert_eq!(SubInstant::try_from("00:18:01,755").unwrap().to_string(), "00:18:01,755"); |
|
|
|
|
assert_eq!(SubInstant::try_from("-00:18:01,755").unwrap().to_string(), "-00:18:01,755"); |
|
|
|
|
assert_eq!(SubInstant::try_from("-00:18:01.7").unwrap().to_string(), "-00:18:01,700"); |
|
|
|
|
assert_eq!(SubInstant::try_from("00:00:00,000").unwrap().to_string(), "00:00:00,000"); |
|
|
|
|
assert_eq!(SubInstant::try_from("-00:00:00,000").unwrap().to_string(), "-00:00:00,000"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn test_stringify_duration() { |
|
|
|
|
assert_eq!(SubDuration::try_from("18:01,755").unwrap().to_string(), "18:01,755"); |
|
|
|
|
assert_eq!(SubDuration::try_from("-18:01,755").unwrap().to_string(), "-18:01,755"); |
|
|
|
|
assert_eq!(SubDuration::try_from("-18:01.7").unwrap().to_string(), "-18:01,700"); |
|
|
|
|
assert_eq!(SubDuration::try_from("-12:18:01.7").unwrap().to_string(), "-12:18:01,700"); |
|
|
|
|
assert_eq!(SubDuration::try_from("0,000").unwrap().to_string(), "0,000"); |
|
|
|
|
assert_eq!(SubDuration::try_from("-0,000").unwrap().to_string(), "-0,000"); |
|
|
|
|
} |
|
|
|
|