package NGCP::BulkProcessor::Calendar; use strict; ## no critic use DateTime qw(); use Time::HiRes qw(); #prevent warning from Time::Warp use Time::Warp qw(); use DateTime::TimeZone qw(); use DateTime::Format::Strptime qw(); use DateTime::Format::ISO8601 qw(); use NGCP::BulkProcessor::Logging qw( getlogger faketimedebug faketimeinfo); use NGCP::BulkProcessor::LogError qw( faketimeerror ); require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw( set_fake_time get_fake_now get_fake_now_string fake_current_unix infinite_future is_infinite_future infinite_past is_infinite_past datetime_to_string datetime_from_string set_timezone current_local from_epoch ); my $is_fake_time = 0; my $timezone_cache = {}; my $UTC = DateTime::TimeZone->new(name => 'UTC'); my $LOCAL = DateTime::TimeZone->new(name => 'local'); my $FLOATING = DateTime::TimeZone::Floating->new(); sub set_fake_time { my ($o) = @_; if (defined $o) { _set_fake_time($o); my $now = _current_local(); faketimeinfo("applying fake time offset '$o' - current time: " . datetime_to_string($now),getlogger(__PACKAGE__)); } else { _set_fake_time(); my $now = _current_local(); faketimeinfo("resetting fake time - current time: " . datetime_to_string($now),getlogger(__PACKAGE__)); } } sub get_fake_now_string { return datetime_to_string(_current_local()); } sub get_fake_now { return _current_local(); } sub fake_current_unix { if ($is_fake_time) { return Time::Warp::time; } else { time; } } sub _current_local { if ($is_fake_time) { return DateTime->from_epoch(epoch => Time::Warp::time, time_zone => $LOCAL); } else { return DateTime->now(time_zone => $LOCAL); } } sub from_epoch { my ($time,$tz) = @_; $tz //= $LOCAL; return DateTime->from_epoch(epoch => $time, time_zone => $tz); } sub current_local { return DateTime->now(time_zone => $LOCAL); } sub infinite_future { #... to '9999-12-31 23:59:59' return DateTime->new(year => 9999, month => 12, day => 31, hour => 23, minute => 59, second => 59, #applying the 'local' timezone takes too long -> "The current implementation of DateTime::TimeZone #will use a huge amount of memory calculating all the DST changes from now until the future date. #Use UTC or the floating time zone and you will be safe." time_zone => $UTC #- with floating timezones, the long conversion takes place when comparing with a 'local' dt #- the error due to leap years/seconds is not relevant in comparisons ); } sub is_infinite_future { my $dt = shift; return $dt->year >= 9999; } sub infinite_past { #mysql 5.5: The supported range is '1000-01-01 00:00:00' ... return DateTime->new(year => 1000, month => 1, day => 1, hour => 0, minute => 0, second => 0, time_zone => $UTC ); #$dt->epoch calls should be okay if perl >= 5.12.0 } sub is_infinite_past { my $dt = shift; return $dt->year <= 1000; } sub datetime_to_string { my ($dt) = @_; return unless defined ($dt); my $s = $dt->ymd('-') . ' ' . $dt->hms(':'); $s .= '.'.$dt->millisecond if $dt->millisecond > 0.0; return $s; } sub datetime_from_string { my ($s,$tz) = @_; $s =~ s/^(\d{4}\-\d{2}\-\d{2})\s+(\d.+)$/$1T$2/; my $ts = DateTime::Format::ISO8601->parse_datetime($s); return set_timezone($ts,$tz); } sub set_timezone { my ($dt,$tz) = @_; return unless defined ($dt); if (defined $tz and length($tz) > 0) { my $timezone; if (exists $timezone_cache->{$tz}) { $timezone = $timezone_cache->{$tz}; } else { $timezone = DateTime::TimeZone->new(name => $tz); $timezone_cache->{$tz} = $timezone; } $dt->set_time_zone( $timezone ); } else { #floating otherwise. $dt->set_time_zone( $FLOATING ); } return $dt; } sub _set_fake_time { my ($o) = @_; $is_fake_time = 1; if (defined $o) { if (ref $o eq 'DateTime') { $o = $o->epoch; } else { my %mult = ( s => 1, m => 60, h => 60*60, d => 60*60*24, M => 60*60*24*30, y => 60*60*24*365, ); if (!$o) { $o = time; } elsif ($o =~ m/^([+-]\d+)([smhdMy]?)$/) { $o = time + $1 * $mult{ $2 || "s" }; } elsif ($o !~ m/\D/) { } else { faketimeerror("Invalid time offset: '$o'",getlogger(__PACKAGE__)); } } Time::Warp::to($o); } else { Time::Warp::reset(); $is_fake_time = 0; } } 1;