fixed and improved
This commit is contained in:
+383
-277
@@ -5,7 +5,7 @@ extern crate lazy_static;
|
|||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::{BufReader, Write};
|
use std::io::{BufReader, Write as ioWrite, StdoutLock, StdinLock};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
@@ -13,6 +13,7 @@ use std::io::BufRead;
|
|||||||
use std::ops::{Add, Mul, MulAssign, AddAssign, SubAssign, Sub};
|
use std::ops::{Add, Mul, MulAssign, AddAssign, SubAssign, Sub};
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
use serde::export::fmt::Debug;
|
use serde::export::fmt::Debug;
|
||||||
|
use core::fmt::Write as fmtWrite;
|
||||||
|
|
||||||
const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"];
|
const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"];
|
||||||
const SPAMMY_LIBS: [&str; 5] = ["tokio_reactor", "hyper", "reqwest", "mio", "want"];
|
const SPAMMY_LIBS: [&str; 5] = ["tokio_reactor", "hyper", "reqwest", "mio", "want"];
|
||||||
@@ -24,14 +25,21 @@ fn main() {
|
|||||||
.about("Modify a .srt file to match a video. Input and output can be a file or stream, \
|
.about("Modify a .srt file to match a video. Input and output can be a file or stream, \
|
||||||
so you pipe multiple invocations to create more complex operations. However, a single \
|
so you pipe multiple invocations to create more complex operations. However, a single \
|
||||||
invocation should suffice in most cases.\n\
|
invocation should suffice in most cases.\n\
|
||||||
Times can be specified in any format: \
|
\n\
|
||||||
seconds (400, 14.52), hours:minutes:seconds (14:00, 15:51.12, 1:30:00). Decimal point \
|
Times are specified with colons and always include seconds (HH:MM:SS, MM:SS, 0:SS). \
|
||||||
can be period or comma; Times copied directly from the .srt file will also work.\n\
|
Decimal point can be either period or comma, so times can be copied directly from the \
|
||||||
When a command allows both time and index as a value, index must be prefixed with '@'.\
|
.srt file. Numbers without colons are assumed to be subtitle indices.\n\
|
||||||
The tool should be used iteratively, adjusting the invocation until the generated \
|
\n\
|
||||||
subtitle file meets expectations. As such, times and indices accepted by its parameters \
|
The tool can be used iteratively, adjusting the invocation until the generated \
|
||||||
are, by default, the ones seen in the output file. Prefix a time or index with '^' \
|
subtitle file matches the audio track. As such, times accepted by its parameters \
|
||||||
to use the original value from the input instead (i.e. original index 14 is '^@14')
|
are, by default, the ones seen in the output file (after shifts and moving), while \
|
||||||
|
indices are those from the input file.\n\
|
||||||
|
\n\
|
||||||
|
Indices are normally not renumbered, so the output file can be used as a reference \
|
||||||
|
for both times and indices. 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 a mismatch. They \
|
||||||
|
can be used when piped into a new process, of course.
|
||||||
")
|
")
|
||||||
.arg(clap::Arg::with_name("input")
|
.arg(clap::Arg::with_name("input")
|
||||||
.value_name("INFILE")
|
.value_name("INFILE")
|
||||||
@@ -43,43 +51,46 @@ fn main() {
|
|||||||
.value_name("OUTFILE")
|
.value_name("OUTFILE")
|
||||||
.help("Output file, defaults to stdout"),
|
.help("Output file, defaults to stdout"),
|
||||||
)
|
)
|
||||||
.arg(clap::Arg::with_name("from")
|
// .arg(clap::Arg::with_name("drop")
|
||||||
.short("f")
|
// .short("d")
|
||||||
.long("from-time")
|
// .long("drop")
|
||||||
.value_name("TIME")
|
// .value_name("ITEMS")
|
||||||
.help("Time of the first affected entry, or its index. Defaults to 0/^@0."),
|
// .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")
|
.arg(clap::Arg::with_name("move")
|
||||||
.short("m")
|
.short("m")
|
||||||
.long("move")
|
.long("move")
|
||||||
.value_name("TIME")
|
.value_name("OFFSET")
|
||||||
.help("Move subtitles in time. Use seconds, M:S or H:M:S, decimals and minus \
|
.help("Move all subtitles in time (e.g 12:00.15 or -0:44)"),
|
||||||
are supported. Starts at the point specified by '--from'"),
|
|
||||||
)
|
)
|
||||||
.arg(clap::Arg::with_name("automove")
|
.arg(clap::Arg::with_name("automove")
|
||||||
.short("M")
|
.short("M")
|
||||||
.long("automove")
|
.long("automove")
|
||||||
.value_name("SUBTIME=VIDEOTIME")
|
.value_name("ENTRY=VIDEOTIME")
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.help("Move subtitles following a given time or index to match video times. \
|
.help("Move subtitles starting at a given time or index to align with \
|
||||||
This automatically sets '--from' if not given explicitly."),
|
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")
|
.arg(clap::Arg::with_name("scale")
|
||||||
.short("s")
|
.short("s")
|
||||||
.long("scale")
|
.long("scale")
|
||||||
.value_name("RATIO")
|
.value_name("RATIO")
|
||||||
.help("Scale subtitle times and durations to compensate for bitrate \
|
.help("Scale all subtitle times and durations to compensate for bitrate \
|
||||||
differences. 1 means identity, 1.1 makes all times 10% longer. Scaling is \
|
differences. 1 means identity, 1.1 makes all times 10% longer. Scaling is \
|
||||||
relative to the first emitted subtitle with positive time (after shifting). \
|
relative to the first emitted subtitle; align it with '--move'. This option \
|
||||||
Has no effect if '--autoscale' is used."),
|
has no effect if '--autoscale' is used."),
|
||||||
)
|
)
|
||||||
.arg(clap::Arg::with_name("autoscale")
|
.arg(clap::Arg::with_name("autoscale")
|
||||||
.short("S")
|
.short("S")
|
||||||
.long("autoscale")
|
.long("autoscale")
|
||||||
.value_name("SUBTIME=VIDEOTIME")
|
.value_name("SUBTIME=VIDEOTIME")
|
||||||
.help("Calculate scaling based on a perceived difference. The scaling is \
|
.help("Calculate scaling based on a perceived difference. The scaling is \
|
||||||
related to the first emitted subtitle, so ensure it is aligned properly \
|
related to the first emitted subtitle; align it with '--move'. \
|
||||||
with '--move'."),
|
This overrides '--scale'."),
|
||||||
)
|
)
|
||||||
.arg(clap::Arg::with_name("durscale")
|
.arg(clap::Arg::with_name("durscale")
|
||||||
.short("d")
|
.short("d")
|
||||||
@@ -92,10 +103,10 @@ fn main() {
|
|||||||
.arg(clap::Arg::with_name("renumber")
|
.arg(clap::Arg::with_name("renumber")
|
||||||
.short("r")
|
.short("r")
|
||||||
.long("renumber")
|
.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(
|
.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();
|
.get_matches();
|
||||||
|
|
||||||
@@ -128,48 +139,25 @@ fn main() {
|
|||||||
}
|
}
|
||||||
builder.init();
|
builder.init();
|
||||||
|
|
||||||
let from = match argv.value_of("from") {
|
|
||||||
Some(mut s) => {
|
|
||||||
if s.starts_with('^') {
|
|
||||||
s = &s[1..];
|
|
||||||
if s.starts_with('@') {
|
|
||||||
let index = &s[1..].parse().expect("Bad --from format");
|
|
||||||
FromTag::ByIndexOrig(*index)
|
|
||||||
} else {
|
|
||||||
// this is always the orig time
|
|
||||||
FromTag::ByTime(SubDuration::try_from(s).expect("Bad --from format").as_instant())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if s.starts_with('@') {
|
|
||||||
let index = &s[1..].parse().expect("Bad --from format");
|
|
||||||
FromTag::ByIndex(*index)
|
|
||||||
} else{
|
|
||||||
FromTag::ByTime(SubDuration::try_from(s).expect("Bad --from format").as_instant())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => FromTag::ByIndex(0)
|
|
||||||
};
|
|
||||||
|
|
||||||
let shift = match argv.value_of("move") {
|
let shift = match argv.value_of("move") {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
SubDuration::try_from(s).expect("Bad --move format")
|
SubDuration::try_from(s).expect("Bad --move format")
|
||||||
}
|
}
|
||||||
None => SubDuration(0f32)
|
None => SubDuration(0f64)
|
||||||
};
|
};
|
||||||
|
|
||||||
let scale = match argv.value_of("scale") {
|
let scale = match argv.value_of("scale") {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
s.parse().expect("Bad --scale format")
|
s.parse().expect("Bad --scale format")
|
||||||
}
|
}
|
||||||
None => 1f32
|
None => 1f64
|
||||||
};
|
};
|
||||||
|
|
||||||
let durscale = match argv.value_of("durscale") {
|
let durscale = match argv.value_of("durscale") {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
s.parse().expect("Bad --durscale format")
|
s.parse().expect("Bad --durscale format")
|
||||||
}
|
}
|
||||||
None => 1f32
|
None => 1f64
|
||||||
}; // always also shrink durations
|
}; // always also shrink durations
|
||||||
|
|
||||||
let autoscale = match argv.value_of("autoscale") {
|
let autoscale = match argv.value_of("autoscale") {
|
||||||
@@ -179,11 +167,11 @@ fn main() {
|
|||||||
panic!("Bad --autoscale format, should be SUBTIME=VIDEOTIME")
|
panic!("Bad --autoscale format, should be SUBTIME=VIDEOTIME")
|
||||||
}
|
}
|
||||||
let (first, second) = (halves[0], halves[1]);
|
let (first, second) = (halves[0], halves[1]);
|
||||||
if first.starts_with('^') {
|
if !first.contains(':') || !second.contains(':') {
|
||||||
panic!("'--autoscale' always uses original times");
|
panic!("'--autoscale' requires two times");
|
||||||
}
|
}
|
||||||
let subtime = SubDuration::try_from(first).expect("Bad --autoscale format").as_instant();
|
let subtime = SubDuration::try_from(first).expect("Bad --autoscale time format").as_instant();
|
||||||
let vidtime = SubDuration::try_from(second).expect("Bad --autoscale format").as_instant();
|
let vidtime = SubDuration::try_from(second).expect("Bad --autoscale time format").as_instant();
|
||||||
|
|
||||||
Some((subtime, vidtime))
|
Some((subtime, vidtime))
|
||||||
}
|
}
|
||||||
@@ -191,57 +179,68 @@ fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut automove = Vec::<AutoMoveTag>::new();
|
let mut automove = Vec::<AutoMoveTag>::new();
|
||||||
|
let mut automove_indices = vec![];
|
||||||
match argv.values_of("automove") {
|
match argv.values_of("automove") {
|
||||||
Some(ss) => {
|
Some(ss) => {
|
||||||
for s in ss {
|
for s in ss {
|
||||||
let halves: Vec<&str> = s.split("=").collect();
|
let halves: Vec<&str> = s.split("=").collect();
|
||||||
if halves.len() != 2 {
|
if halves.len() != 2 {
|
||||||
panic!("Bad --automove format, should be SUBTIME=VIDEOTIME")
|
panic!("Bad --automove format, should be ENTRY=VIDEOTIME")
|
||||||
|
}
|
||||||
|
let (first, second) = (halves[0], halves[1]);
|
||||||
|
if !second.contains(':') {
|
||||||
|
panic!("'--automove' requires time after '='");
|
||||||
}
|
}
|
||||||
let (mut first, second) = (halves[0], halves[1]);
|
|
||||||
let vidtime = SubDuration::try_from(second).expect("Bad --automove format").as_instant();
|
let vidtime = SubDuration::try_from(second).expect("Bad --automove format").as_instant();
|
||||||
|
|
||||||
if first.starts_with('^') {
|
if first.contains(':') {
|
||||||
first = &first[1..];
|
let subtime = SubDuration::try_from(first).expect("Bad --automove format").as_instant();
|
||||||
if s.starts_with('@') {
|
automove.push(AutoMoveTag::ByTime(subtime, vidtime));
|
||||||
let index = &first[1..].parse().expect("Bad --automove format");
|
|
||||||
automove.push(AutoMoveTag::ByIndexOrig(*index, vidtime));
|
|
||||||
} else {
|
|
||||||
let subtime = SubDuration::try_from(first).expect("Bad --automove format").as_instant();
|
|
||||||
automove.push(AutoMoveTag::ByTimeOrig(subtime, vidtime));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if s.starts_with('@') {
|
let index : u32 = first.parse().expect("Bad --automove format");
|
||||||
let index = &first[1..].parse().expect("Bad --automove format");
|
|
||||||
automove.push(AutoMoveTag::ByIndex(*index, vidtime));
|
if automove_indices.contains(&index) {
|
||||||
} else{
|
panic!("Index {} already used in automove.", index);
|
||||||
let subtime = SubDuration::try_from(first).expect("Bad --automove format").as_instant();
|
|
||||||
automove.push(AutoMoveTag::ByTime(subtime, vidtime));
|
|
||||||
}
|
}
|
||||||
|
automove_indices.push(index);
|
||||||
|
|
||||||
|
automove.push(AutoMoveTag::ByIndex(index, vidtime));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => (/* no automoves */)
|
None => (/* no automoves */)
|
||||||
}
|
}
|
||||||
|
debug!("Automove: {:?}", automove);
|
||||||
|
|
||||||
let inf = argv.value_of("input");
|
let inf = argv.value_of("input");
|
||||||
let outf = argv.value_of("output");
|
let outf = argv.value_of("output");
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
let stdout = io::stdout();
|
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 => {
|
None => {
|
||||||
Box::new(stdin.lock().lines())
|
Box::new(StdinSubsInput::new(stdin.lock()))
|
||||||
}
|
}
|
||||||
Some(f) => {
|
Some(f) => {
|
||||||
let file = File::open(f).expect(&format!("Could not open file: {:?}", 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 => {
|
None => {
|
||||||
Box::new(stdout.lock())
|
Box::new(StdoutSubsOutput::new(stdout.lock()))
|
||||||
}
|
}
|
||||||
Some(f) => {
|
Some(f) => {
|
||||||
let file = OpenOptions::new()
|
let file = OpenOptions::new()
|
||||||
@@ -250,65 +249,130 @@ fn main() {
|
|||||||
.write(true)
|
.write(true)
|
||||||
.open(f)
|
.open(f)
|
||||||
.expect(&format!("Could not open file: {:?}", 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,
|
|
||||||
automove,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//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)]
|
#[derive(Debug)]
|
||||||
struct TransformOpts {
|
struct TransformOpts {
|
||||||
renumber: bool,
|
renumber: bool,
|
||||||
autoscale: Option<(SubInstant, SubInstant)>,
|
autoscale: Option<(SubInstant, SubInstant)>,
|
||||||
durscale: f32,
|
durscale: f64,
|
||||||
scale: f32,
|
scale: f64,
|
||||||
shift: SubDuration,
|
shift: SubDuration,
|
||||||
automove: Vec<AutoMoveTag>,
|
automove: Vec<AutoMoveTag>,
|
||||||
from: FromTag,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum AutoMoveTag {
|
enum AutoMoveTag {
|
||||||
ByTime(SubInstant, SubInstant),
|
ByTime(SubInstant, SubInstant),
|
||||||
ByTimeOrig(SubInstant, SubInstant),
|
ByIndex(u32, SubInstant)
|
||||||
ByIndex(u32, SubInstant),
|
|
||||||
ByIndexOrig(u32, SubInstant),
|
|
||||||
ByIndexRelative(u32, SubDuration),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum FromTag {
|
|
||||||
ByTime(SubInstant),
|
|
||||||
ByIndex(u32),
|
|
||||||
ByIndexOrig(u32)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug,Default,Clone,Copy)]
|
#[derive(Debug,Default,Clone,Copy)]
|
||||||
struct IterState {
|
struct IterState {
|
||||||
start_time : Option<SubInstant>,
|
start_time : Option<SubInstant>,
|
||||||
renumber_i : u32,
|
renumber_i : u32,
|
||||||
|
timeline_head : SubInstant,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io::Error>> + 'a>,
|
fn transform_subtitles<'a>(mut lines : Box<dyn SubsInput + 'a>, mut outfile : Box<dyn SubsOutput + 'a>, mut opts : TransformOpts) {
|
||||||
outfile : &mut Box<dyn Write + 'a>,
|
|
||||||
mut opts : TransformOpts) {
|
|
||||||
debug!("Opts: {:#?}", opts);
|
debug!("Opts: {:#?}", opts);
|
||||||
|
|
||||||
let mut istate = IterState::default();
|
let mut istate = IterState::default();
|
||||||
|
|
||||||
let mut linebuf = vec![];
|
let mut linebuf : Vec<String> = vec![];
|
||||||
while let Some(Ok(x)) = lines.next() {
|
'lines: while let Some(x) = lines.next() {
|
||||||
let mut x = x.trim();
|
let mut x = x.trim();
|
||||||
if x.starts_with('\u{feff}') {
|
if x.starts_with('\u{feff}') {
|
||||||
debug!("Stripping BOM mark");
|
debug!("Stripping BOM mark");
|
||||||
@@ -319,16 +383,14 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let istate_backup = istate;
|
|
||||||
|
|
||||||
// 236
|
// 236
|
||||||
// 00:18:01,755 --> 00:18:03,774
|
// 00:18:01,755 --> 00:18:03,774
|
||||||
// (掃除機の音)
|
// (掃除機の音)
|
||||||
// う~ん…。
|
// う~ん…。
|
||||||
match u32::from_str(x) {
|
match u32::from_str(x) {
|
||||||
Ok(num_orig) => {
|
Ok(num) => {
|
||||||
// println!("Entry {}", num);
|
// println!("Entry {}", num);
|
||||||
let datesrow = lines.next().unwrap().unwrap();
|
let datesrow = lines.next().unwrap();
|
||||||
if datesrow.contains(" --> ") {
|
if datesrow.contains(" --> ") {
|
||||||
let mut halves = datesrow.split(" --> ");
|
let mut halves = datesrow.split(" --> ");
|
||||||
let (first, second) = (halves.next().unwrap(), halves.next().unwrap());
|
let (first, second) = (halves.next().unwrap(), halves.next().unwrap());
|
||||||
@@ -336,132 +398,111 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io:
|
|||||||
let sub_end = SubInstant::try_from(second).unwrap();
|
let sub_end = SubInstant::try_from(second).unwrap();
|
||||||
|
|
||||||
linebuf.clear();
|
linebuf.clear();
|
||||||
while let Some(Ok(x)) = lines.next() {
|
'text: while let Some(x) = lines.next() {
|
||||||
if x.is_empty() {
|
if x.is_empty() {
|
||||||
break; // space between the entries
|
break 'text; // space between the entries
|
||||||
}
|
}
|
||||||
linebuf.push(x);
|
linebuf.push(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
let num_new = if opts.renumber {
|
|
||||||
istate.renumber_i += 1;
|
|
||||||
istate.renumber_i
|
|
||||||
} else {
|
|
||||||
num_orig
|
|
||||||
};
|
|
||||||
|
|
||||||
// advance numbering only for the really emitted entries
|
|
||||||
|
|
||||||
|
|
||||||
let mut subtitle = Subtitle {
|
let mut subtitle = Subtitle {
|
||||||
num : num_new,
|
num,
|
||||||
start: sub_start,
|
start: sub_start,
|
||||||
dur: sub_end - sub_start,
|
dur: sub_end - sub_start,
|
||||||
text: linebuf.join("\n"),
|
text: linebuf.join("\n"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if match opts.from {
|
if istate.start_time.is_none() {
|
||||||
FromTag::ByTime(ins) => ins <= sub_start,
|
debug!("Scaling anchored at {} (#{})", sub_start, num);
|
||||||
FromTag::ByIndex(idx) => idx <= num_new,
|
istate.start_time = Some(sub_start);
|
||||||
FromTag::ByIndexOrig(idx) => idx <= num_orig,
|
|
||||||
} {
|
|
||||||
if istate.start_time.is_none() {
|
|
||||||
debug!("Scaling anchored at {} (#{}), editing starts", sub_start, num_orig);
|
|
||||||
debug!("Shifting by: {}", opts.shift);
|
|
||||||
|
|
||||||
istate.start_time = Some(sub_start);
|
if let Some((mut subt, mut vidt)) = opts.autoscale {
|
||||||
|
debug!("Autoscale: VT {} -> ST {}", vidt, subt);
|
||||||
if let Some((mut subt, mut vidt)) = opts.autoscale {
|
subt -= sub_start;
|
||||||
debug!("Autoscale: VT {} -> ST {}", vidt, subt);
|
vidt -= sub_start + opts.shift;
|
||||||
subt -= sub_start;
|
if subt.0 <= 0f64 {
|
||||||
vidt -= sub_start + opts.shift;
|
panic!("Error in autoscale, start time is negative or zero.");
|
||||||
if subt.0 <= 0f32 {
|
|
||||||
panic!("Error in autoscale, start time is negative or zero.");
|
|
||||||
}
|
|
||||||
if vidt.0 <= 0f32 {
|
|
||||||
panic!("Error in autoscale, end time is negative or zero.");
|
|
||||||
}
|
|
||||||
debug!(" relative to #{}, after \"move\": VT {} -> ST {}", num_orig, vidt, subt);
|
|
||||||
opts.scale = vidt.0 / subt.0;
|
|
||||||
debug!("Resolved scale as {}", opts.scale);
|
|
||||||
}
|
}
|
||||||
|
if vidt.0 <= 0f64 {
|
||||||
opts.durscale *= opts.scale;
|
panic!("Error in autoscale, end time is negative or zero.");
|
||||||
debug!("Duration scaling is {}", opts.durscale);
|
}
|
||||||
|
debug!(" relative to #{}, after \"move\": VT {} -> ST {}", num, vidt, subt);
|
||||||
|
opts.scale = vidt.0 / subt.0;
|
||||||
|
debug!("Resolved scale as {}", opts.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.scale != 1f32 {
|
opts.durscale *= opts.scale;
|
||||||
subtitle.start = subtitle.start.scale(istate.start_time.unwrap(), opts.scale);
|
debug!("Duration scaling is {}", opts.durscale);
|
||||||
}
|
}
|
||||||
|
|
||||||
subtitle.dur *= opts.durscale;
|
if opts.scale != 1f64 {
|
||||||
subtitle.start += opts.shift;
|
let scaled = subtitle.start.scale(istate.start_time.unwrap(), opts.scale);
|
||||||
|
trace!("Scale #{} ({}) -> {}", num, subtitle.start, scaled);
|
||||||
|
subtitle.start = scaled;
|
||||||
|
}
|
||||||
|
|
||||||
for amove in opts.automove.iter_mut() {
|
subtitle.dur *= opts.durscale;
|
||||||
match amove {
|
|
||||||
AutoMoveTag::ByIndex(idx, ref vidt) => {
|
// TODO prevent durations overlap (will need to buffer one entry)
|
||||||
if num_new >= *idx {
|
|
||||||
debug!("Move by new index starts, reached {}", idx);
|
let would_be_shifted_start = subtitle.start + opts.shift;
|
||||||
let vidt = *vidt;
|
|
||||||
let dif = vidt - subtitle.start;
|
// TODO use drain_filter when stable
|
||||||
subtitle.start = vidt;
|
let mut to_drop = vec![];
|
||||||
std::mem::replace(amove, AutoMoveTag::ByIndexRelative(num_new, dif));
|
for (i, amove) in opts.automove.iter().enumerate() {
|
||||||
} else if *vidt < subtitle.start && *idx > num_new {
|
match amove {
|
||||||
// istate = istate_backup;
|
AutoMoveTag::ByIndex(idx, ref vidt) => {
|
||||||
warn!("Skip overlapped #{} (by index)", num_orig);
|
if num >= *idx {
|
||||||
continue;
|
let dif = *vidt - would_be_shifted_start;
|
||||||
}
|
debug!("Move by index #{} starts at #{} ({}) -> {}, diff {}", *idx, num, subtitle.start, *vidt, dif);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
AutoMoveTag::ByIndexOrig(idx, ref vidt) => {
|
}
|
||||||
if num_orig >= *idx {
|
AutoMoveTag::ByTime(ref subt, ref vidt) => {
|
||||||
debug!("Move by orig index starts, reached {}", idx);
|
if would_be_shifted_start >= *subt {
|
||||||
let vidt = *vidt;
|
let dif = *vidt - *subt;
|
||||||
let dif = vidt - subtitle.start;
|
debug!("Move by time {} starts at #{} ({}) -> {}, diff {}", subt, num, subtitle.start, *vidt, dif);
|
||||||
subtitle.start = vidt;
|
opts.shift += dif;
|
||||||
std::mem::replace(amove, AutoMoveTag::ByIndexRelative(num_new, dif));
|
to_drop.push(i);
|
||||||
} else if *vidt < sub_start && *idx > num_orig {
|
|
||||||
// istate = istate_backup;
|
|
||||||
warn!("Skip overlapped #{} (by orig index)", num_orig);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
AutoMoveTag::ByTime(ref subt, ref vidt) => {
|
|
||||||
if subtitle.start >= *vidt {
|
|
||||||
// TODO verify
|
|
||||||
subtitle.start += *vidt - *subt;
|
|
||||||
} else if *vidt < subtitle.start && *subt > subtitle.start {
|
|
||||||
// istate = istate_backup;
|
|
||||||
warn!("Skip overlapped #{} (by time)", num_orig);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AutoMoveTag::ByTimeOrig(ref subt, ref vidt) => {
|
|
||||||
if sub_start >= *subt {
|
|
||||||
// TODO verify
|
|
||||||
subtitle.start += *vidt - *subt;
|
|
||||||
} else if *vidt < subtitle.start && *subt > sub_start {
|
|
||||||
// istate = istate_backup;
|
|
||||||
warn!("Skip overlapped #{} (by orig time)", num_orig);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// this is used internally
|
|
||||||
AutoMoveTag::ByIndexRelative(ref idx, ref dif) => {
|
|
||||||
if num_new >= *idx {
|
|
||||||
subtitle.start += *dif;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for i in &to_drop {
|
||||||
if subtitle.start.0 < 0f32 {
|
let x = opts.automove.swap_remove(*i);
|
||||||
warn!("Discarding negative time entry #{} @ {:.3}s", subtitle.num, subtitle.start.0);
|
trace!("Clean up: {:?}", x);
|
||||||
istate = istate_backup;
|
}
|
||||||
continue;
|
if !to_drop.is_empty() {
|
||||||
|
debug!("New shift: {}, Timeline head: {}", opts.shift, istate.timeline_head);
|
||||||
}
|
}
|
||||||
|
|
||||||
outfile.write(subtitle.to_string().as_bytes()).expect("failed to write");
|
if opts.shift.0 != 0f64 {
|
||||||
|
let shifted = subtitle.start + opts.shift;
|
||||||
|
trace!("Shift #{} ({}) by {} -> {}", num, subtitle.start, opts.shift, shifted);
|
||||||
|
subtitle.start = shifted;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.is_negative() {
|
||||||
|
warn!("Discarding negative time entry #{} @ {:.3}s", num, sub_start);
|
||||||
|
continue 'lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
istate.timeline_head = subtitle.start;
|
||||||
|
|
||||||
|
if opts.renumber {
|
||||||
|
istate.renumber_i += 1;
|
||||||
|
subtitle.num = istate.renumber_i;
|
||||||
|
}
|
||||||
|
outfile.emit(subtitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -473,12 +514,11 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outfile.flush().unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, PartialOrd)]
|
//region Time types
|
||||||
struct SubInstant(f32);
|
#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
|
struct SubInstant(f64);
|
||||||
|
|
||||||
impl Debug for SubInstant {
|
impl Debug for SubInstant {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
@@ -486,8 +526,14 @@ impl Debug for SubInstant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, PartialOrd)]
|
impl SubInstant {
|
||||||
struct SubDuration(f32);
|
fn is_negative(&self) -> bool {
|
||||||
|
self.0 < 0f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
|
struct SubDuration(f64);
|
||||||
|
|
||||||
impl Debug for SubDuration {
|
impl Debug for SubDuration {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
@@ -497,7 +543,28 @@ impl Debug for SubDuration {
|
|||||||
|
|
||||||
impl Display for SubDuration {
|
impl Display for SubDuration {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,35 +592,35 @@ impl Sub<SubInstant> for SubInstant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mul<f32> for SubDuration {
|
impl Mul<f64> for SubDuration {
|
||||||
type Output = SubDuration;
|
type Output = SubDuration;
|
||||||
|
|
||||||
fn mul(self, rhs: f32) -> Self::Output {
|
fn mul(self, rhs: f64) -> Self::Output {
|
||||||
SubDuration(self.0 * rhs)
|
SubDuration(self.0 * rhs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MulAssign<f32> for SubDuration {
|
impl MulAssign<f64> for SubDuration {
|
||||||
fn mul_assign(&mut self, rhs: f32) {
|
fn mul_assign(&mut self, rhs: f64) {
|
||||||
self.0 *= rhs;
|
self.0 *= rhs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddAssign<f32> for SubDuration {
|
impl AddAssign<f64> for SubDuration {
|
||||||
fn add_assign(&mut self, rhs: f32) {
|
fn add_assign(&mut self, rhs: f64) {
|
||||||
self.0 += rhs;
|
self.0 += rhs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubInstant {
|
impl SubInstant {
|
||||||
/// Scale by a factor with a custom start time
|
/// 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)
|
SubInstant(start.0 + (self.0 - start.0) * factor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddAssign<f32> for SubInstant {
|
impl AddAssign<f64> for SubInstant {
|
||||||
fn add_assign(&mut self, rhs: f32) {
|
fn add_assign(&mut self, rhs: f64) {
|
||||||
self.0 += rhs;
|
self.0 += rhs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,6 +643,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 {
|
impl AddAssign<SubDuration> for SubInstant {
|
||||||
fn add_assign(&mut self, rhs: SubDuration) {
|
fn add_assign(&mut self, rhs: SubDuration) {
|
||||||
self.0 += rhs.0;
|
self.0 += rhs.0;
|
||||||
@@ -584,19 +663,19 @@ impl AddAssign<SubDuration> for SubInstant {
|
|||||||
|
|
||||||
impl Display for SubInstant {
|
impl Display for SubInstant {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
// TODO optimize this
|
|
||||||
let sign = self.0.signum();
|
let sign = self.0.signum();
|
||||||
let mut secs = self.0.abs();
|
let mut secs = self.0.abs();
|
||||||
let hours = (secs / 3600f32).floor();
|
let hours = (secs / 3600f64).floor();
|
||||||
secs -= hours * 3600f32;
|
secs -= hours * 3600f64;
|
||||||
let minutes = (secs / 60f32).floor();
|
let minutes = (secs / 60f64).floor();
|
||||||
secs -= minutes * 60f32;
|
secs -= minutes * 60f64;
|
||||||
let msecs = ((secs % 1f32) * 1000f32).round();
|
let msecs = ((secs % 1f64) * 1000f64).round();
|
||||||
write!(f, "{}{:02}:{:02}:{:02},{:03}",
|
write!(f, "{}{:02}:{:02}:{:02},{:03}",
|
||||||
if sign.is_sign_negative() { "-" } else { "" },
|
if sign.is_sign_negative() { "-" } else { "" },
|
||||||
hours, minutes, secs.floor(), msecs)
|
hours, minutes, secs.floor(), msecs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct Subtitle {
|
struct Subtitle {
|
||||||
@@ -627,15 +706,15 @@ impl TryFrom<&str> for SubInstant {
|
|||||||
|
|
||||||
match DATE_RE.captures(value) {
|
match DATE_RE.captures(value) {
|
||||||
Some(caps) => {
|
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 h = &caps["h"];
|
||||||
let m = &caps["m"];
|
let m = &caps["m"];
|
||||||
let s = caps["s"].replace(",", ".");
|
let s = caps["s"].replace(",", ".");
|
||||||
|
|
||||||
Ok(SubInstant(minus * (f32::from_str(h).unwrap() * 3600f32 +
|
Ok(SubInstant(minus * (f64::from_str(h).unwrap() * 3600f64 +
|
||||||
f32::from_str(m).unwrap() * 60f32 +
|
f64::from_str(m).unwrap() * 60f64 +
|
||||||
f32::from_str(&s).unwrap())))
|
f64::from_str(&s).unwrap())))
|
||||||
}
|
}
|
||||||
None => Err(failure::format_err!("Error parsing time: {}", value))
|
None => Err(failure::format_err!("Error parsing time: {}", value))
|
||||||
}
|
}
|
||||||
@@ -651,21 +730,29 @@ impl SubDuration {
|
|||||||
impl TryFrom<&str> for SubDuration {
|
impl TryFrom<&str> for SubDuration {
|
||||||
type Error = failure::Error;
|
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! {
|
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) {
|
match TIME_RE.captures(value) {
|
||||||
Some(caps) => {
|
Some(caps) => {
|
||||||
let minus = if caps.name("n").is_some() { -1f32 } else { 1f32 };
|
let minus = if negative { -1f64 } else { 1f64 };
|
||||||
let h = caps.name("h").map_or(0f32, |m| f32::from_str(m.as_str()).unwrap());
|
let h = caps.name("h").map_or(0f64, |m| f64::from_str(m.as_str()).unwrap());
|
||||||
let m = caps.name("m").map_or(0f32, |m| f32::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(0f32, |m| f32::from_str(&m.as_str().replace(",", ".")).unwrap());
|
let s = caps.name("s").map_or(0f64, |m| f64::from_str(&m.as_str().replace(",", ".")).unwrap());
|
||||||
|
|
||||||
Ok(SubDuration(minus * (h * 3600f32 +
|
Ok(SubDuration(minus * (h * 3600f64 + m * 60f64 + s)))
|
||||||
m * 60f32 +
|
|
||||||
s)))
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
Err(failure::format_err!("Error parsing time: {}", value))
|
Err(failure::format_err!("Error parsing time: {}", value))
|
||||||
@@ -677,38 +764,57 @@ impl TryFrom<&str> for SubDuration {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_parse_duration() {
|
fn test_parse_duration() {
|
||||||
// this is used for user input on the command line
|
// this is used for user input on the command line
|
||||||
let bad = SubDuration(-1f32);
|
let bad = SubDuration(-1f64);
|
||||||
assert_eq!(SubDuration(45678f32), SubDuration::try_from("45678").unwrap_or(bad), "integer secs");
|
assert_eq!(SubDuration::try_from(":45678").unwrap_or(bad), SubDuration(45678f64), "integer secs with colon prefix");
|
||||||
assert_eq!(SubDuration(1.23f32), SubDuration::try_from("1.23").unwrap_or(bad), "float secs with period");
|
assert_eq!(SubDuration::try_from("-:45678").unwrap_or(bad), SubDuration(-45678f64), "neg integer secs with colon prefix");
|
||||||
assert_eq!(SubDuration(-1.23f32), SubDuration::try_from("-1.23").unwrap_or(bad), "MINUS float secs with period");
|
assert_eq!(SubDuration::try_from("45678").unwrap_or(bad), SubDuration(45678f64), "integer secs");
|
||||||
assert_eq!(SubDuration(1.23f32), SubDuration::try_from("1,23").unwrap_or(bad), "float secs with comma");
|
assert_eq!(SubDuration::try_from("1.23").unwrap_or(bad), SubDuration(1.23f64), "float secs with period");
|
||||||
assert_eq!(SubDuration(121.15f32), SubDuration::try_from("2:1.15").unwrap_or(bad), "m:s.frac");
|
assert_eq!(SubDuration::try_from("-1.23").unwrap_or(bad), SubDuration(-1.23f64), "MINUS float secs with period");
|
||||||
assert_eq!(SubDuration(121.15f32), SubDuration::try_from("2:01.15").unwrap_or(bad), "m:0s.frac");
|
assert_eq!(SubDuration::try_from("1,23").unwrap_or(bad), SubDuration(1.23f64), "float secs with comma");
|
||||||
assert_eq!(SubDuration(121.15f32), SubDuration::try_from("02:01.15").unwrap_or(bad), "0m:0s.frac");
|
assert_eq!(SubDuration::try_from("2:1.15").unwrap_or(bad), SubDuration(121.15f64), "m:s.frac");
|
||||||
assert_eq!(SubDuration(121.15f32), SubDuration::try_from("02:01,15").unwrap_or(bad), "0m:0s,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(3721.15f32), SubDuration::try_from("1:02:01,15").unwrap_or(bad), "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(3721.15f32), SubDuration::try_from("1:02:01,15").unwrap_or(bad), "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(3721.15f32), SubDuration::try_from("01:02:01,15").unwrap_or(bad), "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(-3721.15f32), SubDuration::try_from("-01:02:01,15").unwrap_or(bad), "-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]
|
#[test]
|
||||||
fn test_parse_instant() {
|
fn test_parse_instant() {
|
||||||
let bad = SubInstant(-1f32);
|
let bad = SubInstant(-1f64);
|
||||||
assert_eq!(SubInstant(1081.755f32), SubInstant::try_from("00:18:01,755").unwrap_or(bad));
|
assert_eq!(SubInstant::try_from("00:18:01,755").unwrap_or(bad), SubInstant(1081.755f64));
|
||||||
assert_eq!(SubInstant(1081.755f32), SubInstant::try_from("00:18:01.755").unwrap_or(bad));
|
assert_eq!(SubInstant::try_from("00:18:01.755").unwrap_or(bad), SubInstant(1081.755f64));
|
||||||
assert_eq!(SubInstant(1081.7f32), SubInstant::try_from("00:18:01.7").unwrap_or(bad));
|
assert_eq!(SubInstant::try_from("00:18:01.7").unwrap_or(bad), SubInstant(1081.7f64));
|
||||||
assert_eq!(SubInstant(1081.7f32), SubInstant::try_from("0:18:1.7").unwrap_or(bad));
|
assert_eq!(SubInstant::try_from("0:18:1.7").unwrap_or(bad), SubInstant(1081.7f64));
|
||||||
assert_eq!(SubInstant(0f32), SubInstant::try_from("00:00:00,000").unwrap_or(bad));
|
assert_eq!(SubInstant::try_from("00:00:00,000").unwrap_or(bad), SubInstant(0f64));
|
||||||
assert_eq!(SubInstant(-3600f32), SubInstant::try_from("-01:00:00,000").unwrap_or(bad));
|
assert_eq!(SubInstant::try_from("-01:00:00,000").unwrap_or(bad), SubInstant(-3600f64));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stringify_instant() {
|
fn test_stringify_instant() {
|
||||||
assert_eq!("00:18:01,755", SubInstant::try_from("00:18:01,755").unwrap().to_string());
|
assert_eq!(SubInstant::try_from("00:18:01,755").unwrap().to_string(), "00:18:01,755");
|
||||||
assert_eq!("-00:18:01,755", SubInstant::try_from("-00:18:01,755").unwrap().to_string());
|
assert_eq!(SubInstant::try_from("-00:18:01,755").unwrap().to_string(), "-00:18:01,755");
|
||||||
assert_eq!("-00:18:01,700", SubInstant::try_from("-00:18:01.7").unwrap().to_string());
|
assert_eq!(SubInstant::try_from("-00:18:01.7").unwrap().to_string(), "-00:18:01,700");
|
||||||
assert_eq!("00:00:00,000", SubInstant::try_from("00:00:00,000").unwrap().to_string());
|
assert_eq!(SubInstant::try_from("00:00:00,000").unwrap().to_string(), "00:00:00,000");
|
||||||
assert_eq!("-00:00:00,000", SubInstant::try_from("-00:00:00,000").unwrap().to_string());
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user