Compare commits

...

4 Commits

  1. 633
      src/main.rs

@ -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");
}

Loading…
Cancel
Save