use std::fmt::Display;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub struct WebVtt {
pub header: VttHeader,
pub blocks: Vec<VttBlock>,
}
impl WebVtt {
pub fn parse(input: &str) -> Result<Self, crate::error::ParseError> {
crate::vtt_parser::vtt(input).map_err(Into::into)
}
pub fn render(&self) -> String {
self.to_string()
}
}
impl Default for WebVtt {
fn default() -> Self {
Self {
header: VttHeader::default(),
blocks: vec![],
}
}
}
impl Display for WebVtt {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "{}\n", self.header)?;
let length = self.blocks.len();
for (i, block) in self.blocks.iter().enumerate() {
if i + 1 < length {
write!(f, "{}\n", block)?;
} else {
write!(f, "{}", block)?;
}
}
Ok(())
}
}
impl Iterator for WebVtt {
type Item = VttBlock;
fn next(&mut self) -> Option<Self::Item> {
if self.blocks.is_empty() {
None
} else {
Some(self.blocks.remove(0))
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VttHeader {
pub description: Option<VttDescription>,
}
impl Default for VttHeader {
fn default() -> Self {
Self {
description: None,
}
}
}
impl Display for VttHeader {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
if let Some(description) = &self.description {
write!(f, "WEBVTT{}\n", description)
} else {
write!(f, "WEBVTT\n")
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum VttDescription {
Side(String),
Below(String),
}
impl Default for VttDescription {
fn default() -> Self {
Self::Side(String::new())
}
}
impl Display for VttDescription {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::Side(description) => {
write!(f, " {}", description)
},
| Self::Below(description) => {
write!(f, "\n{}", description)
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum VttBlock {
Que(VttCue),
Comment(VttComment),
Style(VttStyle),
Region(VttRegion),
}
impl From<VttCue> for VttBlock {
fn from(value: VttCue) -> Self {
VttBlock::Que(value)
}
}
impl From<VttComment> for VttBlock {
fn from(value: VttComment) -> Self {
VttBlock::Comment(value)
}
}
impl From<VttStyle> for VttBlock {
fn from(value: VttStyle) -> Self {
VttBlock::Style(value)
}
}
impl From<VttRegion> for VttBlock {
fn from(value: VttRegion) -> Self {
VttBlock::Region(value)
}
}
impl Display for VttBlock {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::Que(que) => {
write!(f, "{}", que)
},
| Self::Comment(comment) => {
write!(f, "{}", comment)
},
| Self::Style(style) => {
write!(f, "{}", style)
},
| Self::Region(region) => {
write!(f, "{}", region)
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VttRegion {
pub id: Option<RegionId>,
pub width: Option<Percentage>,
pub lines: Option<u32>,
pub region_anchor: Option<Anchor>,
pub viewport_anchor: Option<Anchor>,
pub scroll: Option<Scroll>,
}
pub type RegionId = String;
impl Default for VttRegion {
fn default() -> Self {
Self {
id: None,
width: None,
lines: None,
region_anchor: None,
viewport_anchor: None,
scroll: None,
}
}
}
impl Display for VttRegion {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "REGION\n")?;
if let Some(id) = &self.id {
write!(f, "id:{}\n", id)?;
}
if let Some(width) = self.width {
write!(f, "width:{}\n", width)?;
}
if let Some(lines) = self.lines {
write!(f, "lines:{}\n", lines)?;
}
if let Some(region_anchor) = self.region_anchor {
write!(f, "regionanchor:{}\n", region_anchor)?;
}
if let Some(viewport_anchor) = self.viewport_anchor {
write!(
f,
"viewportanchor:{}\n",
viewport_anchor
)?;
}
if let Some(scroll) = self.scroll {
write!(f, "scroll:{}\n", scroll)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum VttComment {
Side(String),
Below(String),
}
impl Default for VttComment {
fn default() -> Self {
Self::Side(String::new())
}
}
impl Display for VttComment {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::Side(comment) => {
write!(f, "NOTE {}\n", comment)
},
| Self::Below(comment) => {
write!(f, "NOTE\n{}\n", comment)
},
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct VttStyle {
pub style: String,
}
impl Default for VttStyle {
fn default() -> Self {
Self {
style: String::new(),
}
}
}
impl Display for VttStyle {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "STYLE\n{}\n", self.style)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VttCue {
pub identifier: Option<String>,
pub timings: VttTimings,
pub settings: Option<CueSettings>,
pub payload: Vec<String>,
}
impl Default for VttCue {
fn default() -> Self {
Self {
identifier: None,
timings: VttTimings::default(),
settings: None,
payload: vec![],
}
}
}
impl Display for VttCue {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
if let Some(identifier) = &self.identifier {
write!(f, "{}\n", identifier)?;
}
write!(f, "{}", self.timings)?;
if let Some(settings) = &self.settings {
write!(f, " {}", settings)?;
}
write!(f, "\n{}\n", self.payload.join("\n"))
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct VttTimings {
pub start: VttTimestamp,
pub end: VttTimestamp,
}
impl Default for VttTimings {
fn default() -> Self {
Self {
start: VttTimestamp::default(),
end: VttTimestamp::default(),
}
}
}
impl Display for VttTimings {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "{} --> {}", self.start, self.end)
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct VttTimestamp {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
pub milliseconds: u16,
}
impl Default for VttTimestamp {
fn default() -> Self {
Self {
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
}
}
}
impl Display for VttTimestamp {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(
f,
"{:02}:{:02}:{:02}.{:03}",
self.hours, self.minutes, self.seconds, self.milliseconds
)
}
}
impl From<Duration> for VttTimestamp {
fn from(duration: Duration) -> Self {
let seconds = duration.as_secs();
let milliseconds = duration.subsec_millis() as u16;
let hours = (seconds / 3600) as u8;
let minutes = ((seconds % 3600) / 60) as u8;
let seconds = (seconds % 60) as u8;
Self {
hours,
minutes,
seconds,
milliseconds,
}
}
}
impl Into<Duration> for VttTimestamp {
fn into(self) -> Duration {
Duration::new(
self.hours as u64 * 3600
+ self.minutes as u64 * 60
+ self.seconds as u64,
self.milliseconds as u32 * 1_000_000,
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CueSettings {
pub vertical: Option<Vertical>,
pub line: Option<Line>,
pub position: Option<Position>,
pub size: Option<Percentage>,
pub align: Option<Alignment>,
pub region: Option<RegionId>,
}
impl Default for CueSettings {
fn default() -> Self {
Self {
vertical: None,
line: None,
position: None,
size: None,
align: None,
region: None,
}
}
}
impl Display for CueSettings {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let mut settings = Vec::new();
if let Some(vertical) = self.vertical {
settings.push(format!("vertical:{}", vertical));
}
if let Some(line) = self.line {
settings.push(format!("line:{}", line));
}
if let Some(position) = self.position {
settings.push(format!("position:{}", position));
}
if let Some(size) = self.size {
settings.push(format!("size:{}", size));
}
if let Some(align) = self.align {
settings.push(format!("align:{}", align));
}
if let Some(region) = &self.region {
settings.push(format!("region:{}", region));
}
write!(f, "{}", settings.join(" "))
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Percentage {
pub value: f32,
}
impl Default for Percentage {
fn default() -> Self {
Self {
value: 0.0,
}
}
}
impl Display for Percentage {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
if self.value.fract() == 0.0 {
write!(f, "{}%", self.value as i32)
} else {
write!(f, "{}%", self.value)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Anchor {
pub x: Percentage,
pub y: Percentage,
}
impl Default for Anchor {
fn default() -> Self {
Self {
x: Percentage {
value: 0.0,
},
y: Percentage {
value: 100.0,
},
}
}
}
impl Display for Anchor {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "{},{}", self.x, self.y)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Scroll {
Up,
}
impl Default for Scroll {
fn default() -> Self {
Self::Up
}
}
impl Display for Scroll {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::Up => {
write!(f, "up")
},
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Vertical {
Rl,
Lr,
}
impl Default for Vertical {
fn default() -> Self {
Self::Rl
}
}
impl Display for Vertical {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::Rl => {
write!(f, "rl")
},
| Self::Lr => {
write!(f, "lr")
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Line {
Percentage(Percentage, Option<LineAlignment>),
LineNumber(i32, Option<LineAlignment>),
}
impl Default for Line {
fn default() -> Self {
Self::Percentage(Percentage::default(), None)
}
}
impl Display for Line {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::Percentage(percentage, align) => {
if let Some(align) = align {
write!(f, "{},{}", percentage, align)
} else {
write!(f, "{}", percentage)
}
},
| Self::LineNumber(line_number, align) => {
if let Some(align) = align {
write!(f, "{},{}", line_number, align)
} else {
write!(f, "{}", line_number)
}
},
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum LineAlignment {
Start,
Center,
End,
}
impl Default for LineAlignment {
fn default() -> Self {
Self::Start
}
}
impl Display for LineAlignment {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::Start => {
write!(f, "start")
},
| Self::Center => {
write!(f, "center")
},
| Self::End => {
write!(f, "end")
},
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Position {
pub value: Percentage,
pub alignment: Option<PositionAlignment>,
}
impl Default for Position {
fn default() -> Self {
Self {
value: Percentage::default(),
alignment: None,
}
}
}
impl Display for Position {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
if let Some(alignment) = self.alignment {
write!(f, "{},{}", self.value, alignment)
} else {
write!(f, "{}", self.value)
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum PositionAlignment {
LineLeft,
Center,
LineRight,
}
impl Default for PositionAlignment {
fn default() -> Self {
Self::LineLeft
}
}
impl Display for PositionAlignment {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::LineLeft => {
write!(f, "line-left")
},
| Self::Center => {
write!(f, "center")
},
| Self::LineRight => {
write!(f, "line-right")
},
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Alignment {
Start,
Center,
End,
Left,
Right,
}
impl Default for Alignment {
fn default() -> Self {
Self::Start
}
}
impl Display for Alignment {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
| Self::Start => {
write!(f, "start")
},
| Self::Center => {
write!(f, "center")
},
| Self::End => {
write!(f, "end")
},
| Self::Left => {
write!(f, "left")
},
| Self::Right => {
write!(f, "right")
},
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse() {
let text = r#"WEBVTT
00:01.000 --> 00:04.000
- Never drink liquid nitrogen.
00:05.000 --> 00:09.000
- It will perforate your stomach.
- You could die.
"#;
let expected = WebVtt {
blocks: vec![
VttCue {
timings: VttTimings {
start: VttTimestamp {
seconds: 1,
..Default::default()
},
end: VttTimestamp {
seconds: 4,
..Default::default()
},
},
payload: vec!["- Never drink liquid nitrogen.".to_string()],
..Default::default()
}
.into(),
VttCue {
timings: VttTimings {
start: VttTimestamp {
seconds: 5,
..Default::default()
},
end: VttTimestamp {
seconds: 9,
..Default::default()
},
},
payload: vec![
"- It will perforate your stomach.".to_string(),
"- You could die.".to_string(),
],
..Default::default()
}
.into(),
],
..Default::default()
};
assert_eq!(WebVtt::parse(text).unwrap(), expected);
}
#[test]
fn render() {
let vtt = WebVtt {
blocks: vec![
VttCue {
timings: VttTimings {
start: VttTimestamp {
seconds: 1,
..Default::default()
},
end: VttTimestamp {
seconds: 4,
..Default::default()
},
},
payload: vec!["- Never drink liquid nitrogen.".to_string()],
..Default::default()
}
.into(),
VttCue {
timings: VttTimings {
start: VttTimestamp {
seconds: 5,
..Default::default()
},
end: VttTimestamp {
seconds: 9,
..Default::default()
},
},
payload: vec![
"- It will perforate your stomach.".to_string(),
"- You could die.".to_string(),
],
..Default::default()
}
.into(),
],
..Default::default()
};
let expected = r#"WEBVTT
00:00:01.000 --> 00:00:04.000
- Never drink liquid nitrogen.
00:00:05.000 --> 00:00:09.000
- It will perforate your stomach.
- You could die.
"#;
assert_eq!(vtt.render(), expected);
let vtt = WebVtt {
header: VttHeader {
description: Some(VttDescription::Side(
"This is a description.".to_string(),
)),
},
blocks: vec![
VttComment::Side("This is a comment.".to_string()).into(),
VttRegion {
id: Some("region_id".to_string()),
width: Some(Percentage {
value: 50.0,
}),
lines: Some(3),
region_anchor: Some(Anchor {
x: Percentage {
value: 50.0,
},
y: Percentage {
value: 50.0,
},
}),
viewport_anchor: Some(Anchor {
x: Percentage {
value: 50.0,
},
y: Percentage {
value: 50.0,
},
}),
scroll: Some(Scroll::Up),
}
.into(),
VttStyle {
style: r#"video::cue {
background-image: linear-gradient(to bottom, dimgray, lightgray);
color: papayawhip;
}"#
.to_string(),
}
.into(),
VttCue {
identifier: Some("1".to_string()),
timings: VttTimings {
start: VttTimestamp {
hours: 1,
minutes: 2,
seconds: 3,
milliseconds: 4,
},
end: VttTimestamp {
hours: 1,
minutes: 2,
seconds: 5,
milliseconds: 6,
},
},
settings: Some(CueSettings {
vertical: Some(Vertical::Lr),
line: Some(Line::Percentage(
Percentage {
value: 100.0,
},
Some(LineAlignment::Center),
)),
position: Some(Position {
value: Percentage {
value: 50.0,
},
alignment: Some(PositionAlignment::Center),
}),
size: Some(Percentage {
value: 50.0,
}),
align: Some(Alignment::Center),
region: Some("region_id".to_string()),
}),
payload: vec!["- Never drink liquid nitrogen.".to_string()],
}
.into(),
],
};
let expected = r#"WEBVTT This is a description.
NOTE This is a comment.
REGION
id:region_id
width:50%
lines:3
regionanchor:50%,50%
viewportanchor:50%,50%
scroll:up
STYLE
video::cue {
background-image: linear-gradient(to bottom, dimgray, lightgray);
color: papayawhip;
}
1
01:02:03.004 --> 01:02:05.006 vertical:lr line:100%,center position:50%,center size:50% align:center region:region_id
- Never drink liquid nitrogen.
"#;
assert_eq!(vtt.render(), expected);
}
#[test]
fn iterator() {
let vtt = WebVtt {
blocks: vec![
VttCue {
timings: VttTimings {
start: VttTimestamp {
seconds: 1,
..Default::default()
},
end: VttTimestamp {
seconds: 4,
..Default::default()
},
},
payload: vec!["- Never drink liquid nitrogen.".to_string()],
..Default::default()
}
.into(),
VttCue {
timings: VttTimings {
start: VttTimestamp {
seconds: 5,
..Default::default()
},
end: VttTimestamp {
seconds: 9,
..Default::default()
},
},
payload: vec![
"- It will perforate your stomach.".to_string(),
"- You could die.".to_string(),
],
..Default::default()
}
.into(),
],
..Default::default()
};
let mut iter = vtt.into_iter();
assert_eq!(iter.next(), Some(VttCue {
timings: VttTimings {
start: VttTimestamp {
seconds: 1,
..Default::default()
},
end: VttTimestamp {
seconds: 4,
..Default::default()
},
},
payload: vec![
"- Never drink liquid nitrogen.".to_string(),
],
..Default::default()
}.into()));
assert_eq!(
iter.next(),
Some(
VttCue {
timings: VttTimings {
start: VttTimestamp {
seconds: 5,
..Default::default()
},
end: VttTimestamp {
seconds: 9,
..Default::default()
},
},
payload: vec![
"- It will perforate your stomach.".to_string(),
"- You could die.".to_string(),
],
..Default::default()
}
.into()
)
);
assert_eq!(iter.next(), None);
}
#[test]
fn display_header() {
let header = VttHeader {
description: Some(VttDescription::Side(
"This is a description.".to_string(),
)),
};
let expected = "WEBVTT This is a description.\n";
assert_eq!(header.to_string(), expected);
let header = VttHeader {
description: Some(VttDescription::Below(
"This is a description.".to_string(),
)),
};
let expected = "WEBVTT\nThis is a description.\n";
assert_eq!(header.to_string(), expected);
let header = VttHeader {
description: None,
};
let expected = "WEBVTT\n";
assert_eq!(header.to_string(), expected);
}
#[test]
fn display_cue() {
let cue = VttCue {
identifier: Some("1".to_string()),
timings: VttTimings {
start: VttTimestamp {
hours: 0,
minutes: 0,
seconds: 1,
milliseconds: 0,
},
end: VttTimestamp {
hours: 0,
minutes: 0,
seconds: 4,
milliseconds: 0,
},
},
settings: Some(CueSettings {
vertical: Some(Vertical::Lr),
line: Some(Line::Percentage(
Percentage {
value: 100.0,
},
Some(LineAlignment::Center),
)),
position: Some(Position {
value: Percentage {
value: 50.0,
},
alignment: Some(PositionAlignment::Center),
}),
size: Some(Percentage {
value: 50.0,
}),
align: Some(Alignment::Center),
region: Some("region".to_string()),
}),
payload: vec!["- Never drink liquid nitrogen.".to_string()],
};
let expected = "1\n00:00:01.000 --> 00:00:04.000 vertical:lr line:100%,center position:50%,center size:50% align:center region:region\n- Never drink liquid nitrogen.\n";
assert_eq!(cue.to_string(), expected);
let cue = VttCue {
identifier: None,
timings: VttTimings {
start: VttTimestamp {
hours: 0,
minutes: 0,
seconds: 1,
milliseconds: 0,
},
end: VttTimestamp {
hours: 0,
minutes: 0,
seconds: 4,
milliseconds: 0,
},
},
settings: None,
payload: vec!["- Never drink liquid nitrogen.".to_string()],
};
let expected =
"00:00:01.000 --> 00:00:04.000\n- Never drink liquid nitrogen.\n";
assert_eq!(cue.to_string(), expected);
}
#[test]
fn display_comment() {
let comment = VttComment::Side("This is a comment.".to_string());
let expected = "NOTE This is a comment.\n";
assert_eq!(comment.to_string(), expected);
let comment = VttComment::Below("This is a comment.".to_string());
let expected = "NOTE\nThis is a comment.\n";
assert_eq!(comment.to_string(), expected);
let comment =
VttComment::Side("This is a comment.\nacross line.".to_string());
let expected = "NOTE This is a comment.\nacross line.\n";
assert_eq!(comment.to_string(), expected);
}
#[test]
fn display_style() {
let style = VttStyle {
style: "This is a style.".to_string(),
};
let expected = "STYLE\nThis is a style.\n";
assert_eq!(style.to_string(), expected);
}
#[test]
fn display_region() {
let region = VttRegion {
id: Some("region".to_string()),
width: Some(Percentage {
value: 50.0,
}),
lines: Some(3),
region_anchor: Some(Anchor {
x: Percentage {
value: 50.0,
},
y: Percentage {
value: 50.0,
},
}),
viewport_anchor: Some(Anchor {
x: Percentage {
value: 50.0,
},
y: Percentage {
value: 50.0,
},
}),
scroll: Some(Scroll::Up),
};
let expected = "REGION\nid:region\nwidth:50%\nlines:3\nregionanchor:50%,50%\nviewportanchor:50%,50%\nscroll:up\n";
assert_eq!(region.to_string(), expected);
let region = VttRegion {
id: Some("region".to_string()),
width: Some(Percentage {
value: 50.0,
}),
lines: None,
region_anchor: None,
viewport_anchor: None,
scroll: None,
};
let expected = "REGION\nid:region\nwidth:50%\n";
assert_eq!(region.to_string(), expected);
}
#[test]
fn from_duration_to_timestamp() {
let duration = Duration::new(1, 0);
let timestamp: VttTimestamp = duration.into();
assert_eq!(
timestamp,
VttTimestamp {
seconds: 1,
..Default::default()
}
);
let duration = Duration::new(1, 500_000_000);
let timestamp: VttTimestamp = duration.into();
assert_eq!(
timestamp,
VttTimestamp {
seconds: 1,
milliseconds: 500,
..Default::default()
}
);
}
#[test]
fn from_timestamp_to_duration() {
let timestamp = VttTimestamp {
seconds: 1,
..Default::default()
};
let duration: Duration = timestamp.into();
assert_eq!(duration, Duration::new(1, 0));
let timestamp = VttTimestamp {
seconds: 1,
milliseconds: 500,
..Default::default()
};
let duration: Duration = timestamp.into();
assert_eq!(duration, Duration::new(1, 500_000_000));
}
#[test]
fn operate_timestamp_via_duration() {
let start: Duration = VttTimestamp {
seconds: 1,
..Default::default()
}
.into();
let end: Duration = VttTimestamp {
seconds: 4,
..Default::default()
}
.into();
let duration = end - start;
assert_eq!(duration, Duration::new(3, 0));
let timestamp: VttTimestamp = (start + duration).into();
assert_eq!(
timestamp,
VttTimestamp {
seconds: 4,
..Default::default()
}
);
}
#[test]
fn order_timestamp() {
let start = VttTimestamp {
seconds: 1,
..Default::default()
};
let end = VttTimestamp {
seconds: 4,
..Default::default()
};
assert!(start < end);
}
}