File Coverage

blib/lib/WWW/MetaForge/ArcRaiders/Result/EventTimer.pm
Criterion Covered Total %
statement 51 63 80.9
branch 9 22 40.9
condition 3 5 60.0
subroutine 13 15 86.6
pod 9 9 100.0
total 85 114 74.5


line stmt bran cond sub pod time code
1             package WWW::MetaForge::ArcRaiders::Result::EventTimer;
2             our $AUTHORITY = 'cpan:GETTY';
3             # ABSTRACT: Event timer/schedule result object
4             our $VERSION = '0.002';
5 6     6   1251947 use Moo;
  6         9810  
  6         81  
6 6     6   5296 use Types::Standard qw(Str ArrayRef HashRef Maybe InstanceOf);
  6         164899  
  6         66  
7 6     6   36242 use DateTime;
  6         2877280  
  6         309  
8 6     6   6375 use WWW::MetaForge::ArcRaiders::Result::EventTimer::TimeSlot;
  6         27  
  6         294  
9 6     6   47 use namespace::clean;
  6         14  
  6         47  
10              
11             has name => (
12             is => 'ro',
13             isa => Str,
14             required => 1,
15             );
16              
17             has map => (
18             is => 'ro',
19             isa => Str,
20             required => 1,
21             );
22              
23             has icon => (
24             is => 'ro',
25             isa => Maybe[Str],
26             );
27              
28             # Schedule times - array of TimeSlot objects
29             has times => (
30             is => 'ro',
31             isa => ArrayRef[InstanceOf['WWW::MetaForge::ArcRaiders::Result::EventTimer::TimeSlot']],
32             default => sub { [] },
33             );
34              
35             has _raw => (
36             is => 'ro',
37             isa => HashRef,
38             );
39              
40             sub from_hashref {
41 7     7 1 35669 my ($class, $data) = @_;
42              
43             my @slots = map {
44 8         2987 WWW::MetaForge::ArcRaiders::Result::EventTimer::TimeSlot->from_hashref($_)
45 7   100     15 } @{ $data->{times} // [] };
  7         36  
46              
47             return $class->new(
48             name => $data->{name},
49             map => $data->{map},
50             icon => $data->{icon},
51 7         696 times => \@slots,
52             _raw => $data,
53             );
54             }
55              
56             # Build from grouped API data (array of raw event entries with same name+map)
57             sub from_grouped {
58 7     7 1 20091 my ($class, $name, $map, $events) = @_;
59              
60             my @slots = map {
61 7         20 WWW::MetaForge::ArcRaiders::Result::EventTimer::TimeSlot->from_epoch_ms(
62             $_->{startTime}, $_->{endTime}
63             )
64 17         5285 } @$events;
65              
66             # Sort slots by start time
67 7         488 @slots = sort { $a->start <=> $b->start } @slots;
  13         582  
68              
69             return $class->new(
70             name => $name,
71             map => $map,
72             icon => $events->[0]{icon},
73 7         723 times => \@slots,
74             _raw => { name => $name, map => $map, events => $events },
75             );
76             }
77              
78             # Check if event is currently active based on current UTC time
79             sub is_active_now {
80 11     11 1 3420 my ($self) = @_;
81 11         54 my $now = DateTime->now(time_zone => 'UTC');
82              
83 11         4722 for my $slot ($self->times->@*) {
84 26 100       2537 return 1 if $slot->contains($now);
85             }
86 10         1670 return 0;
87             }
88              
89             # Get next scheduled time slot (returns TimeSlot object)
90             sub next_time {
91 7     7 1 296 my ($self) = @_;
92 7         46 my $now = DateTime->now(time_zone => 'UTC');
93              
94 7         3213 my @sorted = sort { $a->start <=> $b->start } $self->times->@*;
  14         727  
95              
96             # Find next slot that starts after now
97 7         544 for my $slot (@sorted) {
98 16 50       938 return $slot if $slot->start > $now;
99             }
100              
101             # Wrap around to first slot tomorrow
102 7 100       496 return $sorted[0] if @sorted;
103 1         9 return undef;
104             }
105              
106             # Get current active time slot (returns TimeSlot object)
107             sub current_slot {
108 1     1 1 196 my ($self) = @_;
109 1         7 my $now = DateTime->now(time_zone => 'UTC');
110              
111 1         399 for my $slot ($self->times->@*) {
112 1 50       6 return $slot if $slot->contains($now);
113             }
114 0         0 return undef;
115             }
116              
117             # Format minutes as human-readable duration
118             sub _format_duration {
119 3     3   10 my ($self, $minutes) = @_;
120 3 50 33     26 return undef unless defined $minutes && $minutes >= 0;
121              
122 0 0       0 if ($minutes < 60) {
123 0         0 return "${minutes}m";
124             }
125 0         0 my $hours = int($minutes / 60);
126 0         0 my $mins = $minutes % 60;
127 0 0       0 return $mins > 0 ? "${hours}h ${mins}m" : "${hours}h";
128             }
129              
130             # Time until next event start
131             sub time_until_start {
132 3     3 1 10 my ($self) = @_;
133 3         11 my $minutes = $self->minutes_until_start;
134 3 50       23 return defined $minutes ? $self->_format_duration($minutes) : undef;
135             }
136              
137             # Minutes until next event start (for sorting)
138             sub minutes_until_start {
139 5     5 1 14 my ($self) = @_;
140 5 50       15 my $next = $self->next_time or return undef;
141 5         30 return $next->minutes_until_start;
142             }
143              
144             # Time until current active slot ends
145             sub time_until_end {
146 0     0 1   my ($self) = @_;
147 0           my $minutes = $self->minutes_until_end;
148 0 0         return defined $minutes ? $self->_format_duration($minutes) : undef;
149             }
150              
151             # Minutes until current active slot ends (for sorting)
152             sub minutes_until_end {
153 0     0 1   my ($self) = @_;
154 0 0         my $slot = $self->current_slot or return undef;
155 0           return $slot->minutes_until_end;
156             }
157              
158             1;
159              
160             __END__
161              
162             =pod
163              
164             =encoding UTF-8
165              
166             =head1 NAME
167              
168             WWW::MetaForge::ArcRaiders::Result::EventTimer - Event timer/schedule result object
169              
170             =head1 VERSION
171              
172             version 0.002
173              
174             =head1 SYNOPSIS
175              
176             my $events = $api->event_timers;
177             for my $event (@$events) {
178             say $event->name;
179             say " Active!" if $event->is_active_now;
180             if (my $next = $event->next_time) {
181             say " Next: ", $next->start, " - ", $next->end;
182             }
183             }
184              
185             =head1 DESCRIPTION
186              
187             Represents an event timer/schedule from the ARC Raiders game.
188              
189             =head2 name
190              
191             Event name.
192              
193             =head2 map
194              
195             Map where event occurs.
196              
197             =head2 icon
198              
199             URL to event icon image.
200              
201             =head2 times
202              
203             ArrayRef of L<WWW::MetaForge::ArcRaiders::Result::EventTimer::TimeSlot> objects.
204              
205             =head2 from_hashref
206              
207             my $event = WWW::MetaForge::ArcRaiders::Result::EventTimer->from_hashref(\%data);
208              
209             Construct from API response (legacy format with times array).
210              
211             =head2 from_grouped
212              
213             my $event = WWW::MetaForge::ArcRaiders::Result::EventTimer->from_grouped(
214             $name, $map, \@events
215             );
216              
217             Construct from grouped API data. Takes an array of raw event entries
218             (with startTime/endTime timestamps) that share the same name and map.
219              
220             =head2 is_active_now
221              
222             if ($event->is_active_now) { ... }
223              
224             Returns true if current time is within a scheduled time slot.
225              
226             =head2 next_time
227              
228             my $slot = $event->next_time;
229             say "Starts at ", $slot->start if $slot;
230              
231             Returns next upcoming TimeSlot object, or undef if none scheduled.
232              
233             =head2 current_slot
234              
235             my $slot = $event->current_slot;
236             say "Ends at ", $slot->end if $slot;
237              
238             Returns currently active TimeSlot object, or undef if not active.
239              
240             =head2 time_until_start
241              
242             my $duration = $event->time_until_start;
243             say "Event starts in $duration" if $duration;
244              
245             Returns human-readable duration until next event start (e.g., "2h 30m", "45m").
246             Returns undef if no upcoming events.
247              
248             =head2 minutes_until_start
249              
250             my $minutes = $event->minutes_until_start;
251              
252             Returns numeric minutes until next event start. Useful for sorting events.
253             Returns undef if no upcoming events.
254              
255             =head2 time_until_end
256              
257             my $duration = $event->time_until_end;
258             say "Event ends in $duration" if $duration;
259              
260             Returns human-readable duration until current event ends (e.g., "1h 15m").
261             Returns undef if event is not currently active.
262              
263             =head2 minutes_until_end
264              
265             my $minutes = $event->minutes_until_end;
266              
267             Returns numeric minutes until current active event ends. Useful for sorting.
268             Returns undef if event is not currently active.
269              
270             =head1 SUPPORT
271              
272             =head2 Issues
273              
274             Please report bugs and feature requests on GitHub at
275             L<https://github.com/Getty/p5-www-metaforge/issues>.
276              
277             =head2 IRC
278              
279             You can reach Getty on C<irc.perl.org> for questions and support.
280              
281             =head1 CONTRIBUTING
282              
283             Contributions are welcome! Please fork the repository and submit a pull request.
284              
285             =head1 AUTHOR
286              
287             Torsten Raudssus <torsten@raudssus.de>
288              
289             =head1 COPYRIGHT AND LICENSE
290              
291             This software is copyright (c) 2026 by Torsten Raudssus.
292              
293             This is free software; you can redistribute it and/or modify it under
294             the same terms as the Perl 5 programming language system itself.
295              
296             =cut