Packages
vtc
0.3.9
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.9
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.2
0.16.1
0.16.0
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.15
0.13.14
0.13.13
0.13.12
0.13.11
0.13.10
0.13.9
0.13.8
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.10
0.10.9
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.3
0.5.2
0.5.1
0.4.0
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
A SMPTE timecode library for Elixir
Current section
Files
Jump to
Current section
Files
lib/private/parse.ex
defmodule Vtc.Private.Parse do
@moduledoc false
use Ratio
alias Vtc.Framerate
alias Vtc.Private.Consts
alias Vtc.Private.DropFrame
alias Vtc.Source.Frames
alias Vtc.Source.Seconds
alias Vtc.Timecode
alias Vtc.Utils.Rational
@spec parse_frames_string(String.t(), Framerate.t()) :: Frames.result()
def parse_frames_string(value, rate) do
case parse_tc_string(value, rate) do
{:ok, _} = result -> result
{:error, %Timecode.ParseError{reason: :bad_drop_frames}} = error -> error
{:error, _} -> parse_feet_and_frames(value, rate)
end
end
@tc_regex ~r/^(?P<negative>-)?((?P<section_1>[0-9]+)[:|;])?((?P<section_2>[0-9]+)[:|;])?((?P<section_3>[0-9]+)[:|;])?(?P<frames>[0-9]+)$/
@spec parse_tc_string(String.t(), Framerate.t()) :: Frames.result()
def parse_tc_string(value, rate) do
with {:ok, matched} <- apply_regex(@tc_regex, value) do
matched
|> tc_matched_to_sections()
|> tc_sections_to_frames(rate)
end
end
@spec apply_regex(Regex.t(), String.t()) :: {:ok, map()} | {:error, Timecode.ParseError.t()}
defp apply_regex(regex, value) do
regex
|> Regex.named_captures(value)
|> then(fn
matched when is_map(matched) -> {:ok, matched}
nil -> {:error, %Timecode.ParseError{reason: :unrecognized_format}}
end)
end
# Extract TC sections from regex match.
@spec tc_matched_to_sections(map()) :: Timecode.Sections.t()
defp tc_matched_to_sections(matched) do
negative? = Map.fetch!(matched, "negative") == "-"
sections = extract_time_sections(matched, 3)
{seconds, sections} = pop_time_section(sections)
{minutes, sections} = pop_time_section(sections)
{hours, _} = pop_time_section(sections)
frames = matched |> Map.fetch!("frames") |> String.to_integer()
%Timecode.Sections{
negative?: negative?,
hours: hours,
minutes: minutes,
seconds: seconds,
frames: frames
}
end
# Extracts a set of sections in a time string of format xx:yy:.. that may or may not
# be truncated at the head.
#
# The regex matches are expected to have a series of fields like "section_1",
# "section_2", etc that denote present sections whose meaning depends on the number
# of sections present.
@spec extract_time_sections(map(), non_neg_integer()) :: [String.t()]
defp extract_time_sections(regex_matches, section_count) do
1..section_count
|> Enum.map(&Integer.to_string/1)
|> Enum.reduce([], fn section_index, sections ->
case Map.fetch!(regex_matches, "section_#{section_index}") do
"" -> sections
this_section -> [this_section | sections]
end
end)
end
# Pops the next section at the end of the list and returns it as an integer.
#
# Returns `0` if the value is not present
@spec pop_time_section([String.t()]) :: {integer(), [String.t()]}
defp pop_time_section(["" | remaining]), do: {0, remaining}
defp pop_time_section([value | remaining]), do: {String.to_integer(value), remaining}
defp pop_time_section([]), do: {0, []}
# Converts all TC fields to a total frame count
@spec tc_sections_to_frames(Timecode.Sections.t(), Framerate.t()) :: Frames.result()
defp tc_sections_to_frames(sections, rate) do
with {:ok, adjustment} <- DropFrame.parse_adjustment(sections, rate) do
frames_per_second = Framerate.timebase(rate)
sections.seconds
|> Ratio.add(sections.minutes * Consts.seconds_per_minute())
|> Ratio.add(sections.hours * Consts.seconds_per_hour())
|> Ratio.mult(frames_per_second)
|> Ratio.add(sections.frames)
|> Ratio.add(adjustment)
|> Rational.round()
|> then(fn frames -> if sections.negative?, do: -frames, else: frames end)
|> then(&{:ok, &1})
end
end
@ff_regex ~r/(?P<negative>-)?(?P<feet>[0-9]+)\+(?P<frames>[0-9]+)/
@spec parse_feet_and_frames(String.t(), Framerate.t()) :: Frames.result()
defp parse_feet_and_frames(value, rate) do
with {:ok, groups} <- apply_regex(@ff_regex, value) do
negative? = Map.fetch!(groups, "negative") == "-"
feet = groups |> Map.fetch!("feet") |> String.to_integer()
groups
|> Map.fetch!("frames")
|> String.to_integer()
|> Ratio.add(feet * Consts.frames_per_foot())
|> then(fn frames -> if negative?, do: -frames, else: frames end)
|> Frames.frames(rate)
end
end
@runtime_regex ~r/^(?P<negative>-)?((?P<section_1>[0-9]+)[:|;])?((?P<section_2>[0-9]+)[:|;])?(?P<seconds>[0-9]+(\.[0-9]+)?)$/
@spec parse_runtime_string(String.t(), Framerate.t()) :: Seconds.result()
def parse_runtime_string(value, rate) do
with {:ok, matched} <- apply_regex(@runtime_regex, value) do
matched
|> runtime_matched_to_second()
|> Seconds.seconds(rate)
end
end
@spec runtime_matched_to_second(map()) :: Rational.t()
defp runtime_matched_to_second(matched) do
negative? = Map.fetch!(matched, "negative") == "-"
sections = extract_time_sections(matched, 2)
{minutes, sections} = pop_time_section(sections)
{hours, _} = pop_time_section(sections)
matched
|> Map.fetch!("seconds")
|> Decimal.new()
|> Ratio.new(1)
|> Ratio.add(hours * Consts.seconds_per_hour())
|> Ratio.add(minutes * Consts.seconds_per_minute())
|> then(fn seconds -> if negative?, do: -seconds, else: seconds end)
end
end