File Coverage

blib/lib/Time/Timecode.pm
Criterion Covered Total %
statement 128 128 100.0
branch 37 42 88.1
condition 31 49 63.2
subroutine 38 38 100.0
pod 5 5 100.0
total 239 262 91.2


line stmt bran cond sub pod time code
1             package Time::Timecode;
2              
3 3     3   268437 use strict;
  3         7  
  3         103  
4 3     3   14 use warnings;
  3         7  
  3         465  
5             use overload
6 3         32 '+' => \&_add,
7             '-' => \&_subtract,
8             '*' => \&_multiply,
9             '/' => \&_divide,
10 3     3   4781 '""' => \&to_string;
  3         3282  
11              
12 3     3   244 use Carp;
  3         6  
  3         690  
13              
14             our $VERSION = '0.02';
15              
16             our $DEFAULT_FPS = 30;
17             our $DEFAULT_DROPFRAME = 0;
18             our $DEFAULT_DELIMITER = ':';
19             our $DEFAULT_FRAME_DELIMITER = $DEFAULT_DELIMITER;
20              
21             my $SECONDS_PER_MINUTE = 60;
22             my $SECONDS_PER_HOUR = $SECONDS_PER_MINUTE * 60;
23              
24             my $TIME_PART = qr|[0-5]\d|;
25             my $DROP_FRAME_DELIMITERS = '.;';
26             my $FRAME_PART_DELIMITERS = "${DEFAULT_DELIMITER}${DROP_FRAME_DELIMITERS}";
27             my $TO_STRING_FORMAT = '%02s%s%02s%s%02s%s%02s';
28              
29             {
30 3     3   16 no strict 'refs';
  3         6  
  3         7521  
31              
32             my @methods = qw|hours minutes seconds frames fps is_dropframe total_frames|;
33             my %method_aliases = (
34             hours => ['hh', 'hrs'],
35             minutes => ['mm', 'mins'],
36             seconds => ['ss', 'secs'],
37             frames => ['ff']
38             );
39              
40             for my $accessor (@methods) {
41 1089     1089   5290 *$accessor = sub { (shift)->{"_$accessor"} };
42             *$_ = \&$accessor for @{$method_aliases{$accessor}};
43             }
44             }
45              
46             sub new
47             {
48 53 50 33 53 1 5932 croak 'usage: Time::Timecode->new( TIMECODE [, OPTIONS ] )' if @_ < 2 || !defined($_[1]);
49              
50 53         70 my $class = shift;
51 53 100       188 my $options = UNIVERSAL::isa($_[-1], 'HASH') ? pop : {};
52 53   66     411 my $self = bless { _is_dropframe => $options->{dropframe},
      66        
53             _frame_delimiter => $options->{frame_delimiter},
54             _delimiter => $options->{delimiter} || $DEFAULT_DELIMITER,
55             _fps => $options->{fps} || $DEFAULT_FPS }, $class;
56              
57 53 50       402 croak "Invalid fps '$self->{_fps}': fps must be > 0" unless $self->{_fps} =~ /^\d+(?:\.\d+)?$/;
58              
59 53 100 100     340 if(@_ == 1 && $_[0] !~ /^\d+$/) {
60 11         27 $self->_timecode_from_string( shift );
61             }
62             else {
63             # For string timecodes these can be derrived by their format
64 42   66     136 $self->{_is_dropframe} ||= $DEFAULT_DROPFRAME;
65 42   66     130 $self->{_frame_delimiter} ||= $DEFAULT_FRAME_DELIMITER;
66            
67 42 100       73 if( @_ == 1 ) {
68 31         71 $self->_timecode_from_total_frames( shift );
69             }
70             else {
71 11 100       30 push @_, 0 unless @_ == 4; # Add frames if necessary
72 11         42 $self->_set_and_validate_time(@_);
73             }
74             }
75              
76 50         191 $self;
77             }
78              
79             sub to_string
80             {
81 8     8 1 108 my $self = shift;
82              
83             #TODO: timecode suffix if string arg to constructor had one
84 8         16 sprintf($TO_STRING_FORMAT,
85             $self->hours,
86             $self->{_delimiter},
87             $self->minutes,
88             $self->{_delimiter},
89             $self->seconds,
90             $self->{_frame_delimiter},
91             $self->frames);
92             }
93              
94             sub convert
95             {
96 2     2 1 4 my ($self, $fps, $options) = @_;
97              
98 2   100     9 $options ||= {};
99 2         4 $options->{fps} = $fps;
100 2   100     7 $options->{dropframe} ||= 0;
101 2   33     10 $options->{delimiter} ||= $self->{_delimiter};
102 2   66     8 $options->{frame_delimiter} ||= $self->{_frame_delimiter};
103              
104 2         5 Time::Timecode->new($self->to_non_dropframe->total_frames, $options);
105             }
106              
107             sub to_dropframe
108             {
109 1     1 1 1 my $self = shift;
110 1 50       2 return $self if $self->is_dropframe;
111              
112 1         2 my $options = $self->_dup_options;
113 1         2 $options->{dropframe} = 1;
114              
115 1         3 Time::Timecode->new($self->total_frames, $options);
116             }
117              
118             sub to_non_dropframe
119             {
120 3     3 1 5 my $self = shift;
121 3 100       6 return $self unless $self->is_dropframe;
122              
123 2         5 my $options = $self->_dup_options;
124 2         5 $options->{dropframe} = 0;
125              
126 2         6 Time::Timecode->new($self->total_frames, $options);
127             }
128              
129              
130             sub _add
131             {
132             _handle_binary_overload(@_, sub {
133 3     3   10 $_[0] + $_[1];
134 3     3   17 });
135             }
136              
137             sub _subtract
138             {
139             _handle_binary_overload(@_, sub {
140 3     3   8 $_[0] - $_[1];
141 3     3   16 });
142             }
143              
144             sub _multiply
145             {
146             _handle_binary_overload(@_, sub {
147 1     1   3 $_[0] * $_[1];
148 2     2   12 });
149             }
150              
151             sub _divide
152             {
153             _handle_binary_overload(@_, sub {
154 2     2   8 int($_[0] / $_[1]);
155 2     2   11 });
156             }
157              
158             sub _handle_binary_overload
159             {
160 10     10   21 my ($lhs, $rhs, $reversed, $fx) = @_;
161              
162 10 100       44 $rhs = Time::Timecode->new($rhs) unless UNIVERSAL::isa($rhs, 'Time::Timecode');
163 9 100       22 ($lhs, $rhs) = ($rhs, $lhs) if $reversed;
164              
165 9         20 Time::Timecode->new($fx->($lhs->total_frames, $rhs->total_frames), $lhs->_dup_options);
166             }
167              
168             sub _dup_options
169             {
170 12     12   17 my $self = shift;
171 12         23 { fps => $self->fps,
172             dropframe => $self->is_dropframe,
173             delimiter => $self->{_delimiter},
174             frame_delimiter => $self->{_frame_delimiter} };
175             }
176              
177             # We work with 10 minute blocks of frames to accommodate dropframe calculations.
178             # Dropframe timecodes call for 2 frames to be added every minute except on the 10th minute.
179             # See REFERENCES in the below POD.
180              
181             sub _frames_per_hour
182             {
183 176     176   205 my $self = shift;
184 176         298 my $fph = $self->_rounded_fps * $SECONDS_PER_HOUR;
185              
186 176 100       386 $fph -= 108 if $self->is_dropframe;
187 176         402 $fph;
188             }
189              
190             sub _frames_per_minute
191             {
192 114     114   127 my $self = shift;
193 114         178 my $fpm = $self->_rounded_fps * $SECONDS_PER_MINUTE;
194              
195 114 100       211 $fpm -= 2 if $self->is_dropframe;
196 114         257 $fpm;
197             }
198              
199             sub _frames_per_ten_minutes
200             {
201 145     145   158 my $self = shift;
202 145         216 my $fpm = $self->_rounded_fps * $SECONDS_PER_MINUTE * 10;
203              
204 145 100       306 $fpm -= 18 if $self->is_dropframe;
205 145         876 $fpm;
206             }
207              
208             sub _frames
209             {
210 31     31   46 my ($self, $frames) = @_;
211 31         52 $self->_frames_without_ten_minute_intervals($frames) % $self->_frames_per_minute % $self->_rounded_fps;
212             }
213              
214             sub _rounded_fps
215             {
216 518     518   525 my $self = shift;
217 518   66     1822 $self->{_rounded_fps} ||= sprintf("%.0f", $self->fps);
218             }
219              
220             sub _hours_from_frames
221             {
222 31     31   55 my ($self, $frames) = @_;
223 31         53 int($frames / $self->_frames_per_hour);
224             }
225              
226             sub _minutes_from_frames
227             {
228 31     31   44 my ($self, $frames) = @_;
229 31         51 my $minutes = int($frames % $self->_frames_per_hour);
230 31         56 int($self->_frames_without_ten_minute_intervals($frames) / $self->_frames_per_minute) + int($minutes / $self->_frames_per_ten_minutes) * 10;
231             }
232              
233             # Needed to handle dropframe calculations
234             sub _frames_without_ten_minute_intervals
235             {
236 93     93   148 my ($self, $frames) = @_;
237 93         158 int($frames % $self->_frames_per_hour % $self->_frames_per_ten_minutes);
238             }
239              
240             sub _seconds_from_frames
241             {
242 31     31   48 my ($self, $frames) = @_;
243 31         56 int($self->_frames_without_ten_minute_intervals($frames) % $self->_frames_per_minute / $self->_rounded_fps);
244             }
245              
246             sub _valid_frames
247             {
248 52     52   74 my ($part, $frames, $max) = @_;
249 52 50 33     486 croak "Invalid frames '$frames': frames must be between 0 and $max" unless $frames =~ /^\d+$/ && $frames >= 0 && $frames <= $max;
      33        
250             }
251              
252             sub _valid_time_part
253             {
254 156     156   194 my ($part, $value) = @_;
255 156 100 33     1163 croak "Invalid $part '$value': $part must be between 0 and 59" if !defined($value) || $value < 0 || $value > 59;
      66        
256             }
257              
258             sub _set_and_validate_time_part
259             {
260 208     208   319 my ($self, $part, $value, $validator) = @_;
261 208         348 $validator->($part, $value, $self->fps);
262 207         582 $self->{"_$part"} = int($value); # Can be a string with a 0 prefix: 01, 02, etc...
263             }
264              
265             sub _set_and_validate_time
266             {
267 21     21   43 my ($self, $hh, $mm, $ss, $ff) = @_;
268              
269 21         56 $self->_set_and_validate_time_part('frames', $ff, \&_valid_frames);
270 21         47 $self->_set_and_validate_time_part('seconds', $ss, \&_valid_time_part);
271 21         45 $self->_set_and_validate_time_part('minutes', $mm, \&_valid_time_part);
272 21         49 $self->_set_and_validate_time_part('hours', $hh, \&_valid_time_part);
273              
274 21         55 my $total = $self->frames;
275 21         41 $total += $self->seconds * $self->_rounded_fps;
276              
277             # These 2 statements are used for calculating dropframe timecodes. They do not affect non-dropframe calculations.
278 21         50 $total += int($self->minutes / 10) * $self->_frames_per_ten_minutes;
279 21         603 $total += $self->minutes % 10 * $self->_frames_per_minute;
280              
281 21         44 $total += $self->hours * $self->_frames_per_hour;
282              
283 21 100       42 croak "Invalid dropframe timecode: '$self'" unless $self->_valid_dropframe_timecode;
284 20         74 $self->{_total_frames} = $total;
285             }
286              
287             sub _valid_dropframe_timecode
288             {
289 51     51   60 my $self = shift;
290 51   66     132 !($self->is_dropframe && $self->seconds == 0 && ($self->frames == 0 || $self->frames == 1) && ($self->minutes % 10 != 0));
291             }
292              
293             sub _set_timecode_from_frames
294             {
295 31     31   48 my ($self, $frames) = @_;
296              
297 31         56 $self->_set_and_validate_time_part('frames', $self->_frames($frames), \&_valid_frames);
298 31         91 $self->_set_and_validate_time_part('seconds', $self->_seconds_from_frames($frames), \&_valid_time_part);
299 31         81 $self->_set_and_validate_time_part('minutes', $self->_minutes_from_frames($frames), \&_valid_time_part);
300 31         72 $self->_set_and_validate_time_part('hours', $self->_hours_from_frames($frames), \&_valid_time_part);
301              
302             #Bump up to valid drop frame... ever?
303 30 50       70 $self->_set_timecode_from_frames($frames + 2) unless $self->_valid_dropframe_timecode
304             }
305              
306             sub _timecode_from_total_frames
307             {
308 31     31   48 my ($self, $frames) = @_;
309 31         65 $self->{_total_frames} = $frames;
310 31         59 $self->_set_timecode_from_frames($frames);
311             }
312              
313             # Close your eyes, it's about to get ugly...
314             sub _timecode_from_string
315             {
316 11     11   15 my ($self, $timecode) = @_;
317 11         29 my $delim = '[' . quotemeta("$self->{_delimiter}$DEFAULT_DELIMITER") . ']';
318 11         14 my $frame_delim = $FRAME_PART_DELIMITERS;
319              
320 11 100       27 $frame_delim .= $self->{_frame_delimiter} if defined $self->{_frame_delimiter};
321 11         19 $frame_delim = '[' . quotemeta("$frame_delim") . ']';
322              
323 11 100       354 if($timecode =~ /^\s*($TIME_PART)$delim($TIME_PART)$delim($TIME_PART)($frame_delim)([0-5]\d)\s*([NDPF])?\s*$/) {
324             #TODO: Use suffix after frames to determine drop/non-drop -and possibly other things
325 10 100 100     64 $self->{_is_dropframe} = 1 unless defined $self->{_is_dropframe} || index($DROP_FRAME_DELIMITERS, $4) == -1;
326 10 100       34 $self->{_frame_delimiter} = $4 unless defined $self->{_frame_delimiter};
327              
328 10         26 $self->_set_and_validate_time($1, $2, $3, $5);
329             }
330             else {
331 1         229 croak "Can't create timecode from '$timecode'";
332             }
333             }
334              
335             1;
336              
337             __END__