File Coverage

lib/Time/StasisField.pm
Criterion Covered Total %
statement 100 100 100.0
branch 41 42 97.6
condition 5 5 100.0
subroutine 27 27 100.0
pod 8 14 57.1
total 181 188 96.2


line stmt bran cond sub pod time code
1             package Time::StasisField;
2              
3             =head1 NAME
4              
5             Time::StasisField - control the flow of time
6              
7             =cut
8              
9 4     4   16219 use strict;
  4         7  
  4         117  
10 4     4   17 use warnings;
  4         5  
  4         101  
11              
12 4     4   3096 use POSIX (qw{SIGALRM});
  4         28205  
  4         22  
13 4     4   4936 use Scalar::Util (qw{set_prototype});
  4         8  
  4         728  
14              
15             =head1 VERSION
16              
17             Version 0.01
18              
19             =cut
20              
21             our $VERSION = '0.01';
22              
23             =head1 SYNOPSIS
24              
25             I provides a simple interface for controlling the flow of
26             time. When the stasis field is disengaged, Perl's core time functions --
27             alarm, gmtime, localtime, sleep, and time -- behave normally, assuming that
28             time flows with the system clock. When the stasis field is engaged, time
29             is guaranteed to advance at a predictable rate on every call. For consistency,
30             all other time-related functions will use the modified time.
31              
32             Example usage:
33              
34             use Time::StasisField;
35              
36             my @foos;
37              
38             @foos = map { Foo->new(create_time => time) } (1 .. 20);
39              
40             # All times will likely all look the same
41             print $foos[-1]->create_time - $foos[0]->create_time;
42              
43             # The program will pause for 10 seconds
44             sleep(10);
45              
46             # Time will be 10 seconds later
47             print time;
48              
49             #Let's control time
50             Time::StasisField->engage;
51              
52             @foos = map { Foo->new(create_time => time) } (1 .. 20);
53              
54             # All times will be distinct
55             print $foos[-1]->create_time - $foos[0]->create_time;
56              
57             # Time will advance by 10 seconds
58             sleep(10);
59              
60             # Fetch the current time without advancing it
61             print Time::StasisField->now;
62              
63              
64             Time::StasisField->seconds_per_tick(60);
65              
66             # Time is now 1 minute later
67             print time;
68              
69             # Everything is back to normal
70             Time::StasisField->disengage;
71              
72             # Hooray for system time
73             print Time::StasisField->now;
74              
75             =cut
76              
77             ############################
78             # Private Class Variables
79             ############################
80              
81             my $alarm_time;
82             my $current_time = 0;
83             my $is_alarm_set = 0;
84             my $is_engaged = 0;
85             my $is_frozen = 0;
86             my $seconds_per_tick = 1;
87              
88             ############################
89             # Helper Functions
90             ############################
91              
92             sub _validate_number {
93 125     125   188 my $class = shift;
94              
95             #Make sure the value is numeric
96 4     4   20 use warnings (FATAL => 'all');
  4         7  
  4         228  
97 4     4   37 no warnings ("void");
  4         16  
  4         2108  
98 125         228 int($_[0]);
99             }
100              
101             sub _trigger_alarm {
102 108     108   125 my $class = shift;
103              
104             return
105 108 100 100     330 if ! $is_alarm_set
106             || $class->now < $alarm_time;
107              
108 8         67 CORE::alarm(0);
109 8         9 $is_alarm_set = 0;
110 8         219 kill SIGALRM, $$;
111             }
112              
113             =head1 STASIS FIELD METHODS
114              
115             =cut
116              
117             =head2 engage
118              
119             Enable the stasis field, seizing control of the system time and setting now to
120             the time the field was enabled. If engage is called while the field is already
121             enabled, now is updated to the current system time.
122              
123             =cut
124              
125             sub engage {
126 17     17 1 2966 my $class = shift;
127              
128 17 100       54 if ($class->is_engaged) {
129             #Update now to real time
130 2         4 $current_time = CORE::time;
131             #Trigger the alarm that may have occurred during the transition
132 2         7 $class->_trigger_alarm;
133              
134             } else {
135             #Turn off the alarm so that we don't accidentally throw while switching state
136 15         54 my $old_alarm = $class->alarm(0);
137              
138 15         28 $is_engaged = 1;
139 15         24 $current_time = CORE::time;
140              
141             #Turn the alarm back on
142 15   100     93 $class->alarm($old_alarm || 0);
143             }
144              
145 16         39 return;
146             }
147              
148             =head2 disenage
149              
150             Disable the stasis field, returning control to the system time.
151              
152             =cut
153              
154             sub disengage {
155 20     20 0 3395 my $class = shift;
156              
157 20 100       99 return unless $class->is_engaged;
158              
159 15         22 $current_time = CORE::time;
160 15         25 $is_engaged = 0;
161              
162             #Start the system alarm from now
163 15 100       48 $class->alarm($alarm_time - $current_time) if $is_alarm_set;
164             #Trigger the alarm that may have occurred during the transition
165 15         42 $class->_trigger_alarm;
166              
167 14         30 return;
168             }
169              
170             =head2 is_engaged
171              
172             Return whether or not the stasis field is enabled.
173              
174             =cut
175              
176 424     424 1 13997854 sub is_engaged { $is_engaged }
177              
178             =head2 freeze
179              
180             Time should stop advancing now.
181              
182             =cut
183              
184 1     1 1 7 sub freeze { $is_frozen = 1 }
185              
186             =head2 unfreeze
187              
188             Time should continue advancing now.
189              
190             =cut
191              
192 1     1 1 3 sub unfreeze { $is_frozen = 0 }
193              
194             =head2 is_frozen
195              
196             Return whether or not time advances now.
197              
198             =cut
199              
200 81     81 1 263 sub is_frozen { $is_frozen }
201              
202             =head1 TIME METHODS
203              
204             =cut
205              
206             =head2 now
207              
208             Accessor for the current time. The supplied time may be any valid number,
209             though now will always return an integer. Falls back to the system time when
210             the stasis field is disengaged.
211              
212             =cut
213              
214             sub now {
215 200     200 1 3446 my $class = shift;
216              
217 200 100       381 return CORE::time unless $class->is_engaged;
218              
219 186 100       453 if (@_) {
220 17         47 $class->_validate_number($_[0]);
221 16         22 $current_time = $_[0];
222 16         43 $class->_trigger_alarm;
223             }
224              
225 185         603 return int($current_time);
226             }
227              
228             =head2 seconds_per_tick
229              
230             Accessor for the number of seconds time changes with each tick. Supports
231             negative and subsecond deltas. Only works on time in an engaged stasis field.
232              
233             =cut
234              
235             sub seconds_per_tick {
236 84     84 1 2707 my $class = shift;
237              
238 84 100       189 if (@_) {
239 8         20 $class->_validate_number($_[0]);
240 8         15 $seconds_per_tick = $_[0];
241             }
242              
243 84         155 return $seconds_per_tick;
244             }
245              
246             =head2 tick
247              
248             Advance time by the value of seconds_per_tick, regardless of the freeze state.
249             Returns now.
250              
251             =cut
252              
253             sub tick {
254 89     89 1 148 my $class = shift;
255              
256 89 100       180 return CORE::time unless $class->is_engaged;
257              
258 75         160 $current_time += $class->seconds_per_tick;
259 75         154 $class->_trigger_alarm;
260              
261 74         182 return $class->now;
262             }
263              
264             ############################
265             # Core Overrides
266             ############################
267              
268             BEGIN {
269 4     4   11 for my $function (qw{
270             alarm
271             gmtime
272             localtime
273             sleep
274             time
275             }) {
276 4     4   21 no strict 'refs';
  4         7  
  4         416  
277 20         758 *{"CORE::GLOBAL::$function"} = set_prototype(
278 154     154   3022655 sub { unshift @_, 'Time::StasisField'; goto &{"Time::StasisField::$function"} },
  154         197  
  154         759  
279 20         377 prototype("CORE::$function")
280             );
281             }
282             }
283              
284             sub alarm {
285 76     76 0 128 my $class = shift;
286 76 50       180 my $offset = @_ ? $_[0] : $_;
287              
288 76         226 $class->_validate_number($offset);
289              
290 76 100       152 return CORE::alarm($offset) unless $class->is_engaged;
291              
292 35 100       117 my $previous_alarm_time_remaining =
    100          
293             ! defined $alarm_time ? $alarm_time :
294             $is_alarm_set ? $alarm_time - $class->now : 0;
295 35 100       110 $alarm_time = $offset > -1 ? $class->now + int($offset) : undef;
296 35         52 $is_alarm_set = $offset >= 1;
297              
298 35         121 return $previous_alarm_time_remaining;
299             }
300              
301             sub gmtime {
302 5     5 0 10 my $class = shift;
303              
304 5 100       21 $class->_validate_number($_[0]) if @_;
305 4     4   20 use warnings (FATAL => 'all');
  4         8  
  4         332  
306 5 100       61 CORE::gmtime(@_ ? $_[0] : time);
307             }
308              
309             sub localtime {
310 5     5 0 12 my $class = shift;
311              
312 5 100       25 $class->_validate_number($_[0]) if @_;
313 4     4   49 use warnings (FATAL => 'all');
  4         5  
  4         850  
314 5 100       263 CORE::localtime(@_ ? $_[0] : time);
315             }
316              
317             sub sleep {
318 20     20 0 57 my $class = shift;
319              
320 20 100       2000249 return CORE::sleep unless @_;
321 18         70 $class->_validate_number($_[0]);
322 18 100       2000178 return CORE::sleep if $_[0] <= -1;
323 16 100       77 return $class->is_engaged ? do { $class->now($class->now + $_[0]); int($_[0]) } : CORE::sleep($_[0]);
  5         20  
  5         35  
324             }
325              
326             sub time {
327 81     81 0 128 my $class = shift;
328              
329 81 100       186 return $class->is_frozen ? $class->now : $class->tick;
330             }
331              
332             =head1 ACKNOWLEDGEMENTS
333              
334             This module was made possible by L
335             (L<@ShutterTech|https://twitter.com/ShutterTech>). Additional open source
336             projects from Shutterstock can be found at
337             L.
338              
339             =head1 AUTHOR
340              
341             Aaron Cohen, C<< >>
342              
343             =head1 BUGS
344              
345             Please report any bugs or feature requests to C, or through
346             the web interface at L. I will
347             be notified, and then you'll automatically be notified of progress on your bug as I make changes.
348              
349             =head1 SUPPORT
350              
351             You can find documentation for this module with the perldoc command.
352              
353             perldoc Time::StasisField
354              
355             You can also look for information at:
356              
357             =over 4
358              
359             =item * Official GitHub Repo
360              
361             L
362              
363             =item * GitHub's Issue Tracker (report bugs here)
364              
365             L
366              
367             =item * CPAN Ratings
368              
369             L
370              
371             =item * Official CPAN Page
372              
373             L
374              
375             =back
376              
377             =head1 LICENSE AND COPYRIGHT
378              
379             Copyright 2013 Aaron Cohen.
380              
381             This program is free software; you can redistribute it and/or modify it
382             under the terms of either: the GNU General Public License as published
383             by the Free Software Foundation; or the Artistic License.
384              
385             See http://dev.perl.org/licenses/ for more information.
386              
387             =cut
388              
389             1; # End of Time::StasisField