1# -- 2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ 3# -- 4# This software comes with ABSOLUTELY NO WARRANTY. For details, see 5# the enclosed file COPYING for license information (GPL). If you 6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. 7# -- 8 9package Kernel::System::DateTime; 10## nofilter(TidyAll::Plugin::OTRS::Perl::Time) 11## nofilter(TidyAll::Plugin::OTRS::Perl::Translatable) 12 13use strict; 14use warnings; 15 16use Exporter qw(import); 17our %EXPORT_TAGS = ( ## no critic 18 all => [ 19 'OTRSTimeZoneGet', 20 'SystemTimeZoneGet', 21 'TimeZoneList', 22 'UserDefaultTimeZoneGet', 23 ], 24); 25Exporter::export_ok_tags('all'); 26 27use DateTime; 28use DateTime::TimeZone; 29use Scalar::Util qw( looks_like_number ); 30use Kernel::System::VariableCheck qw( IsArrayRefWithData IsHashRefWithData ); 31 32our %ObjectManagerFlags = ( 33 NonSingleton => 1, 34 AllowConstructorFailure => 1, 35); 36 37our @ObjectDependencies = ( 38 'Kernel::Config', 39 'Kernel::System::Main', 40 'Kernel::System::Log', 41); 42 43our $Locale = DateTime::Locale->load('en_US'); 44 45use overload 46 '>' => \&_OpIsNewerThan, 47 '<' => \&_OpIsOlderThan, 48 '>=' => \&_OpIsNewerThanOrEquals, 49 '<=' => \&_OpIsOlderThanOrEquals, 50 '==' => \&_OpEquals, 51 '!=' => \&_OpNotEquals, 52 'fallback' => 1; 53 54=head1 NAME 55 56Kernel::System::DateTime - Handles date and time calculations. 57 58=head1 DESCRIPTION 59 60Handles date and time calculations. 61 62=head1 PUBLIC INTERFACE 63 64=head2 new() 65 66Creates a DateTime object. Do not use new() directly, instead use the object manager: 67 68 # Create an object with current date and time 69 # within time zone set in SysConfig OTRSTimeZone: 70 my $DateTimeObject = $Kernel::OM->Create( 71 'Kernel::System::DateTime' 72 ); 73 74 # Create an object with current date and time 75 # within a certain time zone: 76 my $DateTimeObject = $Kernel::OM->Create( 77 'Kernel::System::DateTime', 78 ObjectParams => { 79 TimeZone => 'Europe/Berlin', # optional, TimeZone name. 80 } 81 ); 82 83 # Create an object with a specific date and time: 84 my $DateTimeObject = $Kernel::OM->Create( 85 'Kernel::System::DateTime', 86 ObjectParams => { 87 Year => 2016, 88 Month => 1, 89 Day => 22, 90 Hour => 12, # optional, defaults to 0 91 Minute => 35, # optional, defaults to 0 92 Second => 59, # optional, defaults to 0 93 TimeZone => 'Europe/Berlin', # optional, defaults to setting of SysConfig OTRSTimeZone 94 } 95 ); 96 97 # Create an object from an epoch timestamp. These timestamps are always UTC/GMT, 98 # hence time zone will automatically be set to UTC. 99 # 100 # If parameter Epoch is present, all other parameters will be ignored. 101 my $DateTimeObject = $Kernel::OM->Create( 102 'Kernel::System::DateTime', 103 ObjectParams => { 104 Epoch => 1453911685, 105 } 106 ); 107 108 # Create an object from a date/time string. 109 # 110 # If parameter String is given, Year, Month, Day, Hour, Minute and Second will be ignored 111 my $DateTimeObject = $Kernel::OM->Create( 112 'Kernel::System::DateTime', 113 ObjectParams => { 114 String => '2016-08-14 22:45:00', 115 TimeZone => 'Europe/Berlin', # optional, defaults to setting of SysConfig OTRSTimeZone 116 } 117 ); 118 119 # Following formats for parameter String are supported: 120 # 121 # yyyy-mm-dd hh:mm:ss 122 # yyyy-mm-dd hh:mm # sets second to 0 123 # yyyy-mm-dd # sets hour, minute and second to 0 124 # yyyy-mm-ddThh:mm:ss+tt:zz 125 # yyyy-mm-ddThh:mm:ss+ttzz 126 # yyyy-mm-ddThh:mm:ss-tt:zz 127 # yyyy-mm-ddThh:mm:ss-ttzz 128 # yyyy-mm-ddThh:mm:ss [timezone] # time zone will be deduced from an optional string 129 # yyyy-mm-ddThh:mm:ss[timezone] # i.e. 2018-04-20T07:37:10UTC 130 131=cut 132 133sub new { 134 my ( $Type, %Param ) = @_; 135 136 # allocate new hash for object 137 my $Self = {}; 138 bless( $Self, $Type ); 139 140 # CPAN DateTime: only use English descriptions and abbreviations internally. 141 # This has nothing to do with the user's locale settings in OTRS. 142 $Self->{Locale} = $Locale; 143 144 # Use private parameter to pass in an already created CPANDateTimeObject (used) 145 # by the Clone() method). 146 if ( $Param{_CPANDateTimeObject} ) { 147 $Self->{CPANDateTimeObject} = $Param{_CPANDateTimeObject}; 148 return $Self; 149 } 150 151 # Create the CPAN/Perl DateTime object. 152 my $CPANDateTimeObject = $Self->_CPANDateTimeObjectCreate(%Param); 153 154 if ( ref $CPANDateTimeObject ne 'DateTime' ) { 155 156 # Add debugging information. 157 my $Parameters = $Kernel::OM->Get('Kernel::System::Main')->Dump( 158 \%Param, 159 ); 160 161 # Remove $VAR1 = 162 $Parameters =~ s{ \s* \$VAR1 \s* = \s* \{}{}xms; 163 164 # Remove closing brackets. 165 $Parameters =~ s{\}\s+\{}{\{}xms; 166 $Parameters =~ s{\};\s*$}{}xms; 167 168 # Replace new lines with spaces. 169 $Parameters =~ s{\n}{ }gsmx; 170 171 # Replace multiple spaces with one. 172 $Parameters =~ s{\s+}{ }gsmx; 173 174 $Kernel::OM->Get('Kernel::System::Log')->Log( 175 'Priority' => 'Error', 176 'Message' => "Error creating DateTime object ($Parameters).", 177 ); 178 179 return; 180 } 181 182 $Self->{CPANDateTimeObject} = $CPANDateTimeObject; 183 return $Self; 184} 185 186=head2 Get() 187 188Returns hash ref with the date, time and time zone values of this object. 189 190 my $DateTimeSettings = $DateTimeObject->Get(); 191 192Returns: 193 194 my $DateTimeSettings = { 195 Year => 2016, 196 Month => 1, # starting at 1 197 Day => 22, 198 Hour => 16, 199 Minute => 35, 200 Second => 59, 201 DayOfWeek => 5, # starting with 1 for Monday, ending with 7 for Sunday 202 TimeZone => 'Europe/Berlin', 203 }; 204 205=cut 206 207sub Get { 208 my ( $Self, %Param ) = @_; 209 210 my $Values = { 211 Year => $Self->{CPANDateTimeObject}->year(), 212 Month => $Self->{CPANDateTimeObject}->month(), 213 MonthAbbr => $Self->{CPANDateTimeObject}->month_abbr(), 214 Day => $Self->{CPANDateTimeObject}->day(), 215 Hour => $Self->{CPANDateTimeObject}->hour(), 216 Minute => $Self->{CPANDateTimeObject}->minute(), 217 Second => $Self->{CPANDateTimeObject}->second(), 218 DayOfWeek => $Self->{CPANDateTimeObject}->day_of_week(), 219 DayAbbr => $Self->{CPANDateTimeObject}->day_abbr(), 220 TimeZone => $Self->{CPANDateTimeObject}->time_zone_long_name(), 221 }; 222 223 return $Values; 224} 225 226=head2 Set() 227 228Sets date and time values of this object. You have to give at least one parameter. Only given values will be changed. 229Note that the resulting date and time have to be valid. On validation error, the current date and time of the object 230won't be changed. 231 232Note that in order to change the time zone, you have to use method C<L</ToTimeZone()>>. 233 234 # Setting values by hash: 235 my $Success = $DateTimeObject->Set( 236 Year => 2016, 237 Month => 1, 238 Day => 22, 239 Hour => 16, 240 Minute => 35, 241 Second => 59, 242 ); 243 244 # Settings values by date/time string: 245 my $Success = $DateTimeObject->Set( String => '2016-02-25 20:34:01' ); 246 247If parameter C<String> is present, all other parameters will be ignored. Please see C<L</new()>> for the list of 248supported string formats. 249 250Returns: 251 252 $Success = 1; # On success, or false otherwise. 253 254=cut 255 256sub Set { 257 my ( $Self, %Param ) = @_; 258 259 if ( defined $Param{String} ) { 260 my $DateTimeHash = $Self->_StringToHash( String => $Param{String} ); 261 return if !$DateTimeHash; 262 263 %Param = %{$DateTimeHash}; 264 } 265 266 my @DateTimeParams = qw ( Year Month Day Hour Minute Second ); 267 268 # Check given parameters 269 my $ParamGiven; 270 DATETIMEPARAM: 271 for my $DateTimeParam (@DateTimeParams) { 272 next DATETIMEPARAM if !defined $Param{$DateTimeParam}; 273 274 $ParamGiven = 1; 275 last DATETIMEPARAM; 276 } 277 278 if ( !$ParamGiven ) { 279 $Kernel::OM->Get('Kernel::System::Log')->Log( 280 'Priority' => 'Error', 281 'Message' => 'Missing at least one parameter.', 282 ); 283 return; 284 } 285 286 # Validate given values by using the current settings + the given ones. 287 my $CurrentValues = $Self->Get(); 288 DATETIMEPARAM: 289 for my $DateTimeParam (@DateTimeParams) { 290 next DATETIMEPARAM if !defined $Param{$DateTimeParam}; 291 292 $CurrentValues->{$DateTimeParam} = $Param{$DateTimeParam}; 293 } 294 295 # Create a new DateTime object with the new/added values 296 my $CPANDateTimeParams = $Self->_ToCPANDateTimeParamNames( %{$CurrentValues} ); 297 298 # Delete parameters that are not allowed for set method 299 delete $CPANDateTimeParams->{time_zone}; 300 301 my $Result; 302 eval { 303 $Result = $Self->{CPANDateTimeObject}->set( %{$CPANDateTimeParams} ); 304 }; 305 306 return $Result; 307} 308 309=head2 Add() 310 311Adds duration or working time to date and time of this object. You have to give at least one of the valid parameters. 312On error, the current date and time of this object won't be changed. 313 314 my $Success = $DateTimeObject->Add( 315 Years => 1, 316 Months => 2, 317 Weeks => 4, 318 Days => 34, 319 Hours => 2, 320 Minutes => 5, 321 Seconds => 459, 322 323 # Calculate "destination date" by adding given time values as 324 # working time. Note that for adding working time, 325 # only parameters Seconds, Minutes, Hours and Days are allowed. 326 AsWorkingTime => 0, # set to 1 to add given values as working time 327 328 # Calendar to use for working time calculations, optional 329 Calendar => 9, 330 ); 331 332Returns: 333 334 $Success = 1; # On success, or false otherwise. 335 336=cut 337 338sub Add { 339 my ( $Self, %Param ) = @_; 340 341 # 342 # Check parameters 343 # 344 my @DateTimeParams = qw ( Years Months Weeks Days Hours Minutes Seconds ); 345 @DateTimeParams = qw( Days Hours Minutes Seconds ) if $Param{AsWorkingTime}; 346 347 # Check for needed parameters 348 my $ParamsGiven = 0; 349 my $ParamsValid = 1; 350 DATETIMEPARAM: 351 for my $DateTimeParam (@DateTimeParams) { 352 next DATETIMEPARAM if !defined $Param{$DateTimeParam}; 353 354 if ( !looks_like_number( $Param{$DateTimeParam} ) ) { 355 $ParamsValid = 0; 356 last DATETIMEPARAM; 357 } 358 359 # negative values are not allowed when calculating working time 360 if ( int $Param{$DateTimeParam} < 0 && $Param{AsWorkingTime} ) { 361 $ParamsValid = 0; 362 last DATETIMEPARAM; 363 } 364 365 $ParamsGiven = 1; 366 } 367 368 if ( !$ParamsGiven || !$ParamsValid ) { 369 $Kernel::OM->Get('Kernel::System::Log')->Log( 370 'Priority' => 'Error', 371 'Message' => 'Missing or invalid date/time parameter(s).', 372 ); 373 return; 374 } 375 376 # Check for not allowed parameters 377 my %AllowedParams = map { $_ => 1 } @DateTimeParams; 378 $AllowedParams{AsWorkingTime} = 1; 379 if ( $Param{AsWorkingTime} ) { 380 $AllowedParams{Calendar} = 1; 381 } 382 383 for my $Param ( sort keys %Param ) { 384 if ( !$AllowedParams{$Param} ) { 385 $Kernel::OM->Get('Kernel::System::Log')->Log( 386 'Priority' => 'Error', 387 'Message' => "Parameter $Param is not allowed.", 388 ); 389 return; 390 } 391 } 392 393 # NOTE: For performance reasons, the following code for calculating date and time 394 # works directly with the CPAN DateTime object instead of methods of Kernel::System::DateTime. 395 396 # 397 # Working time calculation 398 # 399 if ( $Param{AsWorkingTime} ) { 400 401 # Combine time parameters to seconds 402 my $RemainingSeconds = 0; 403 if ( defined $Param{Seconds} ) { 404 $RemainingSeconds += int $Param{Seconds}; 405 } 406 if ( defined $Param{Minutes} ) { 407 $RemainingSeconds += int $Param{Minutes} * 60; 408 } 409 if ( defined $Param{Hours} ) { 410 $RemainingSeconds += int $Param{Hours} * 60 * 60; 411 } 412 if ( defined $Param{Days} ) { 413 $RemainingSeconds += int $Param{Days} * 60 * 60 * 24; 414 } 415 416 return if !$RemainingSeconds; 417 418 # Backup current date/time to be able to revert to it in case of failure 419 my $OriginalDateTimeObject = $Self->{CPANDateTimeObject}->clone(); 420 421 my $TimeZone = $OriginalDateTimeObject->time_zone(); 422 423 # Get working and vacation times, use calendar if given 424 my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); 425 my $TimeWorkingHours = $ConfigObject->Get('TimeWorkingHours'); 426 my $TimeVacationDays = $ConfigObject->Get('TimeVacationDays'); 427 my $TimeVacationDaysOneTime = $ConfigObject->Get('TimeVacationDaysOneTime'); 428 if ( 429 $Param{Calendar} 430 && $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} . "Name" ) 431 ) 432 { 433 $TimeWorkingHours = $ConfigObject->Get( "TimeWorkingHours::Calendar" . $Param{Calendar} ); 434 $TimeVacationDays = $ConfigObject->Get( "TimeVacationDays::Calendar" . $Param{Calendar} ); 435 $TimeVacationDaysOneTime = $ConfigObject->Get( 436 "TimeVacationDaysOneTime::Calendar" . $Param{Calendar} 437 ); 438 439 # Switch to time zone of calendar 440 $TimeZone = $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} ) 441 || $Self->OTRSTimeZoneGet(); 442 443 # Use Kernel::System::DateTime's ToTimeZone() here because of error handling 444 # and because performance is irrelevant at this point. 445 if ( !$Self->ToTimeZone( TimeZone => $TimeZone ) ) { 446 $Kernel::OM->Get('Kernel::System::Log')->Log( 447 Priority => 'error', 448 Message => "Error setting time zone $TimeZone.", 449 ); 450 451 return; 452 } 453 } 454 455 # If there are for some reason no working hours configured, stop here 456 # to prevent failing via loop protection below. 457 my $WorkingHoursConfigured; 458 WORKINGHOURCONFIGDAY: 459 for my $WorkingHourConfigDay ( sort keys %{$TimeWorkingHours} ) { 460 if ( IsArrayRefWithData( $TimeWorkingHours->{$WorkingHourConfigDay} ) ) { 461 $WorkingHoursConfigured = 1; 462 last WORKINGHOURCONFIGDAY; 463 } 464 } 465 return 1 if !$WorkingHoursConfigured; 466 467 # Convert $TimeWorkingHours into Hash 468 my %TimeWorkingHours; 469 for my $DayName ( sort keys %{$TimeWorkingHours} ) { 470 $TimeWorkingHours{$DayName} = { map { $_ => 1 } @{ $TimeWorkingHours->{$DayName} } }; 471 } 472 473 # Protection for endless loop 474 my $LoopStartTime = time(); 475 LOOP: 476 while ( $RemainingSeconds > 0 ) { 477 478 # Fail if this loop takes longer than 5 seconds 479 if ( time() - $LoopStartTime > 5 ) { 480 481 # Reset this object to original date/time. 482 $Self->{CPANDateTimeObject} = $OriginalDateTimeObject->clone(); 483 484 $Kernel::OM->Get('Kernel::System::Log')->Log( 485 Priority => 'error', 486 Message => 'Adding working time took too long, aborting.', 487 ); 488 489 return; 490 } 491 492 my $Year = $Self->{CPANDateTimeObject}->year(); 493 my $Month = $Self->{CPANDateTimeObject}->month(); 494 my $Day = $Self->{CPANDateTimeObject}->day(); 495 my $DayName = $Self->{CPANDateTimeObject}->day_abbr(); 496 my $Hour = $Self->{CPANDateTimeObject}->hour(); 497 my $Minute = $Self->{CPANDateTimeObject}->minute(); 498 my $Second = $Self->{CPANDateTimeObject}->second(); 499 500 # Check working times and vacation days 501 my $IsWorkingDay = !$TimeVacationDays->{$Month}->{$Day} 502 && !$TimeVacationDaysOneTime->{$Year}->{$Month}->{$Day} 503 && exists $TimeWorkingHours->{$DayName} 504 && keys %{ $TimeWorkingHours{$DayName} }; 505 506 # On start of day check if whole day can be processed in one chunk 507 # instead of hour by hour (performance reasons). 508 if ( !$Hour && !$Minute && !$Second ) { 509 510 # The following code is slightly faster than using CPAN DateTime's add(), 511 # presumably because add() always creates a DateTime::Duration object. 512 my $Epoch = $Self->{CPANDateTimeObject}->epoch(); 513 $Epoch += 60 * 60 * 24; 514 515 my $NextDayDateTimeObject = DateTime->from_epoch( 516 epoch => $Epoch, 517 time_zone => $TimeZone, 518 locale => $Self->{Locale}, 519 ); 520 521 # Only handle days with exactly 24 hours here 522 if ( 523 !$NextDayDateTimeObject->hour() 524 && !$NextDayDateTimeObject->minute() 525 && !$NextDayDateTimeObject->second() 526 && $NextDayDateTimeObject->day() != $Day 527 ) 528 { 529 my $FullDayProcessed = 1; 530 531 if ($IsWorkingDay) { 532 my $WorkingHours = keys %{ $TimeWorkingHours{$DayName} }; 533 my $WorkingSeconds = $WorkingHours * 60 * 60; 534 535 if ( $RemainingSeconds > $WorkingSeconds ) { 536 $RemainingSeconds -= $WorkingSeconds; 537 } 538 else { 539 $FullDayProcessed = 0; 540 } 541 } 542 543 # Move forward 24 hours if full day has been processed 544 if ($FullDayProcessed) { 545 546 # Time implicitly set to 0 547 $Self->{CPANDateTimeObject}->set( 548 year => $NextDayDateTimeObject->year(), 549 month => $NextDayDateTimeObject->month(), 550 day => $NextDayDateTimeObject->day(), 551 ); 552 553 next LOOP; 554 } 555 } 556 } 557 558 # Calculate remaining seconds of the current hour 559 my $SecondsOfCurrentHour = ( $Minute * 60 ) + $Second; 560 my $SecondsToAdd = ( 60 * 60 ) - $SecondsOfCurrentHour; 561 562 if ( $IsWorkingDay && $TimeWorkingHours{$DayName}->{$Hour} ) { 563 $SecondsToAdd = $RemainingSeconds if $SecondsToAdd > $RemainingSeconds; 564 $RemainingSeconds -= $SecondsToAdd; 565 } 566 567 # The following code is slightly faster than using CPAN DateTime's add(), 568 # presumably because add() always creates a DateTime::Duration object. 569 my $Epoch = $Self->{CPANDateTimeObject}->epoch(); 570 $Epoch += $SecondsToAdd; 571 572 $Self->{CPANDateTimeObject} = DateTime->from_epoch( 573 epoch => $Epoch, 574 time_zone => $TimeZone, 575 locale => $Self->{Locale}, 576 ); 577 } 578 579 # Return to original time zone, might have been changed by calendar 580 $Self->{CPANDateTimeObject}->set_time_zone( $OriginalDateTimeObject->time_zone() ); 581 582 return 1; 583 } 584 585 # 586 # "Normal" date/time calculation 587 # 588 589 # Calculations are only made in UTC/floating time zone to prevent errors with times that 590 # would not exist in the given time zone (e. g. on/around daylight saving time switch). 591 # CPAN DateTime fails if adding days, months or years which would result in a non-existing 592 # time in the given time zone. Converting it to UTC and back has the desired effect. 593 # 594 # Also see http://stackoverflow.com/questions/18489927/a-day-without-midnight 595 my $TimeZone = $Self->{CPANDateTimeObject}->time_zone(); 596 $Self->{CPANDateTimeObject}->set_time_zone('UTC'); 597 598 # Convert to floating time zone to get rid of leap seconds which can lead to times like 23:59:61 599 $Self->{CPANDateTimeObject}->set_time_zone('floating'); 600 601 # Add duration 602 my $DurationParameters = $Self->_ToCPANDateTimeParamNames(%Param); 603 eval { 604 $Self->{CPANDateTimeObject}->add( %{$DurationParameters} ); 605 }; 606 607 # Store possible error before it might get lost by call to ToTimeZone 608 my $Error = $@; 609 610 # First convert floating time zone back to UTC and from there to the original time zone 611 $Self->{CPANDateTimeObject}->set_time_zone('UTC'); 612 $Self->{CPANDateTimeObject}->set_time_zone($TimeZone); 613 614 return if $Error; 615 616 return 1; 617} 618 619=head2 Subtract() 620 621Subtracts duration from date and time of this object. You have to give at least one of the valid parameters. On 622validation error, the current date and time of this object won't be changed. 623 624 my $Success = $DateTimeObject->Subtract( 625 Years => 1, 626 Months => 2, 627 Weeks => 4, 628 Days => 34, 629 Hours => 2, 630 Minutes => 5, 631 Seconds => 459, 632 ); 633 634Returns: 635 636 $Success = 1; # On success, or false otherwise. 637 638=cut 639 640sub Subtract { 641 my ( $Self, %Param ) = @_; 642 643 my @DateTimeParams = qw ( Years Months Weeks Days Hours Minutes Seconds ); 644 645 # Check for needed parameters 646 my $ParamsGiven = 0; 647 my $ParamsValid = 1; 648 DATETIMEPARAM: 649 for my $DateTimeParam (@DateTimeParams) { 650 next DATETIMEPARAM if !defined $Param{$DateTimeParam}; 651 652 if ( !looks_like_number( $Param{$DateTimeParam} ) ) { 653 $ParamsValid = 0; 654 last DATETIMEPARAM; 655 } 656 657 # negative values are not allowed when calculating working time 658 if ( int $Param{$DateTimeParam} < 0 && $Param{AsWorkingTime} ) { 659 $ParamsValid = 0; 660 last DATETIMEPARAM; 661 } 662 663 $ParamsGiven = 1; 664 } 665 666 if ( !$ParamsGiven || !$ParamsValid ) { 667 $Kernel::OM->Get('Kernel::System::Log')->Log( 668 'Priority' => 'Error', 669 'Message' => 'Missing or invalid date/time parameter(s).', 670 ); 671 return; 672 } 673 674 # Check for not allowed parameters 675 my %AllowedParams = map { $_ => 1 } @DateTimeParams; 676 $AllowedParams{AsWorkingTime} = 1; 677 if ( $Param{AsWorkingTime} ) { 678 $AllowedParams{Calendar} = 1; 679 } 680 681 for my $Param ( sort keys %Param ) { 682 if ( !$AllowedParams{$Param} ) { 683 $Kernel::OM->Get('Kernel::System::Log')->Log( 684 'Priority' => 'Error', 685 'Message' => "Parameter $Param is not allowed.", 686 ); 687 return; 688 } 689 } 690 691 # Calculations are only made in UTC/floating time zone to prevent errors with times that 692 # would not exist in the given time zone (e. g. on/around daylight saving time switch). 693 my $DateTimeValues = $Self->Get(); 694 $Self->ToTimeZone( TimeZone => 'UTC' ); 695 696 # Convert to floating time zone to get rid of leap seconds which can lead to times like 23:59:61 697 $Self->{CPANDateTimeObject}->set_time_zone('floating'); 698 699 # Subtract duration 700 my $DurationParameters = $Self->_ToCPANDateTimeParamNames(%Param); 701 eval { 702 $Self->{CPANDateTimeObject}->subtract( %{$DurationParameters} ); 703 }; 704 705 # Store possible error before it might get lost by call to ToTimeZone 706 my $Error = $@; 707 708 # First convert floating time zone back to UTC and from there to the original time zone 709 $Self->{CPANDateTimeObject}->set_time_zone('UTC'); 710 $Self->ToTimeZone( TimeZone => $DateTimeValues->{TimeZone} ); 711 712 return if $@; 713 714 return 1; 715} 716 717=head2 Delta() 718 719Calculates delta between this and another DateTime object. Optionally calculates the working time between the two. 720 721 my $Delta = $DateTimeObject->Delta( DateTimeObject => $AnotherDateTimeObject ); 722 723Note that the returned values are always positive. Use the comparison methods to see if a date is newer/older/equal. 724 725 # Calculate "working time" 726 ForWorkingTime => 0, # set to 1 to calculate working time between the two DateTime objects 727 728 # Calendar to use for working time calculations, optional 729 Calendar => 9, 730 731Returns: 732 733 my $Delta = { 734 Years => 1, # Set to 0 if working time was calculated 735 Months => 2, # Set to 0 if working time was calculated 736 Weeks => 4, # Set to 0 if working time was calculated 737 Days => 34, # Set to 0 if working time was calculated 738 Hours => 2, 739 Minutes => 5, 740 Seconds => 459, 741 AbsoluteSeconds => 42084759, # complete delta in seconds 742 }; 743 744=cut 745 746sub Delta { 747 my ( $Self, %Param ) = @_; 748 749 if ( 750 !defined $Param{DateTimeObject} 751 || ref $Param{DateTimeObject} ne ref $Self 752 ) 753 { 754 $Kernel::OM->Get('Kernel::System::Log')->Log( 755 'Priority' => 'Error', 756 'Message' => "Missing or invalid parameter DateTimeObject.", 757 ); 758 return; 759 } 760 761 my $Delta = { 762 Years => 0, 763 Months => 0, 764 Weeks => 0, 765 Days => 0, 766 Hours => 0, 767 Minutes => 0, 768 Seconds => 0, 769 AbsoluteSeconds => 0, 770 }; 771 772 # 773 # Calculate delta for working time 774 # 775 if ( $Param{ForWorkingTime} ) { 776 777 # NOTE: For performance reasons, the following code for calculating the working time 778 # works directly with the CPAN DateTime object instead of Kernel::System::DateTime. 779 780 # Clone StartDateTime object because it will be changed while calculating 781 # but the original object must not be changed. 782 my $StartDateTimeObject = $Self->{CPANDateTimeObject}->clone(); 783 my $TimeZone = $StartDateTimeObject->time_zone(); 784 785 # Get working and vacation times, use calendar if given 786 my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); 787 my $TimeWorkingHours = $ConfigObject->Get('TimeWorkingHours'); 788 my $TimeVacationDays = $ConfigObject->Get('TimeVacationDays'); 789 my $TimeVacationDaysOneTime = $ConfigObject->Get('TimeVacationDaysOneTime'); 790 if ( 791 $Param{Calendar} 792 && $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} . "Name" ) 793 ) 794 { 795 $TimeWorkingHours = $ConfigObject->Get( "TimeWorkingHours::Calendar" . $Param{Calendar} ); 796 $TimeVacationDays = $ConfigObject->Get( "TimeVacationDays::Calendar" . $Param{Calendar} ); 797 $TimeVacationDaysOneTime = $ConfigObject->Get( 798 "TimeVacationDaysOneTime::Calendar" . $Param{Calendar} 799 ); 800 801 # switch to time zone of calendar 802 $TimeZone = $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} ) 803 || $Self->OTRSTimeZoneGet(); 804 805 eval { 806 $StartDateTimeObject->set_time_zone($TimeZone); 807 }; 808 809 if ($@) { 810 $Kernel::OM->Get('Kernel::System::Log')->Log( 811 Priority => 'error', 812 Message => "Error setting time zone $TimeZone for start DateTime object.", 813 ); 814 815 return; 816 } 817 } 818 819 # If there are for some reason no working hours configured, stop here 820 # to prevent failing via loop protection below. 821 my $WorkingHoursConfigured; 822 WORKINGHOURCONFIGDAY: 823 for my $WorkingHourConfigDay ( sort keys %{$TimeWorkingHours} ) { 824 if ( IsArrayRefWithData( $TimeWorkingHours->{$WorkingHourConfigDay} ) ) { 825 $WorkingHoursConfigured = 1; 826 last WORKINGHOURCONFIGDAY; 827 } 828 } 829 return $Delta if !$WorkingHoursConfigured; 830 831 # Convert $TimeWorkingHours into Hash 832 my %TimeWorkingHours; 833 for my $DayName ( sort keys %{$TimeWorkingHours} ) { 834 $TimeWorkingHours{$DayName} = { map { $_ => 1 } @{ $TimeWorkingHours->{$DayName} } }; 835 } 836 837 my $StartTime = $StartDateTimeObject->epoch(); 838 my $StopTime = $Param{DateTimeObject}->{CPANDateTimeObject}->epoch(); 839 my $WorkingTime = 0; 840 841 # Protection for endless loop 842 my $LoopStartTime = time(); 843 LOOP: 844 while ( $StartTime < $StopTime ) { 845 846 # Fail if this loop takes longer than 5 seconds 847 if ( time() - $LoopStartTime > 5 ) { 848 $Kernel::OM->Get('Kernel::System::Log')->Log( 849 Priority => 'error', 850 Message => 'Delta calculation of working time took too long, aborting.', 851 ); 852 853 return; 854 } 855 856 my $RemainingSeconds = $StopTime - $StartTime; 857 858 my $Year = $StartDateTimeObject->year(); 859 my $Month = $StartDateTimeObject->month(); 860 my $Day = $StartDateTimeObject->day(); 861 my $DayName = $StartDateTimeObject->day_abbr(); 862 my $Hour = $StartDateTimeObject->hour(); 863 my $Minute = $StartDateTimeObject->minute(); 864 my $Second = $StartDateTimeObject->second(); 865 866 # Check working times and vacation days 867 my $IsWorkingDay = !$TimeVacationDays->{$Month}->{$Day} 868 && !$TimeVacationDaysOneTime->{$Year}->{$Month}->{$Day} 869 && exists $TimeWorkingHours->{$DayName} 870 && keys %{ $TimeWorkingHours{$DayName} }; 871 872 # On start of day check if whole day can be processed in one chunk 873 # instead of hour by hour (performance reasons). 874 if ( !$Hour && !$Minute && !$Second ) { 875 876 # The following code is slightly faster than using CPAN DateTime's add(), 877 # presumably because add() always creates a DateTime::Duration object. 878 my $Epoch = $StartDateTimeObject->epoch(); 879 $Epoch += 60 * 60 * 24; 880 881 my $NextDayDateTimeObject = DateTime->from_epoch( 882 epoch => $Epoch, 883 time_zone => $TimeZone, 884 locale => $Self->{Locale}, 885 ); 886 887 # Only handle days with exactly 24 hours here 888 if ( 889 !$NextDayDateTimeObject->hour() 890 && !$NextDayDateTimeObject->minute() 891 && !$NextDayDateTimeObject->second() 892 && $NextDayDateTimeObject->day() != $Day 893 && $RemainingSeconds > 60 * 60 * 24 894 ) 895 { 896 my $FullDayProcessed = 1; 897 898 if ($IsWorkingDay) { 899 my $WorkingHours = keys %{ $TimeWorkingHours{$DayName} }; 900 my $WorkingSeconds = $WorkingHours * 60 * 60; 901 902 if ( $RemainingSeconds > $WorkingSeconds ) { 903 $WorkingTime += $WorkingSeconds; 904 } 905 else { 906 $FullDayProcessed = 0; 907 } 908 } 909 910 # Move forward 24 hours if full day has been processed 911 if ($FullDayProcessed) { 912 913 # Time implicitly set to 0 914 $StartDateTimeObject->set( 915 year => $NextDayDateTimeObject->year(), 916 month => $NextDayDateTimeObject->month(), 917 day => $NextDayDateTimeObject->day(), 918 ); 919 920 $StartTime = $Epoch; 921 922 next LOOP; 923 } 924 } 925 } 926 927 # Calculate remaining seconds of the current hour 928 my $SecondsOfCurrentHour = ( $Minute * 60 ) + $Second; 929 my $SecondsToAdd = ( 60 * 60 ) - $SecondsOfCurrentHour; 930 931 if ( $IsWorkingDay && $TimeWorkingHours{$DayName}->{$Hour} ) { 932 $SecondsToAdd = $RemainingSeconds if $SecondsToAdd > $RemainingSeconds; 933 $WorkingTime += $SecondsToAdd; 934 } 935 936 # The following code is slightly faster than using CPAN DateTime's add(), 937 # presumably because add() always creates a DateTime::Duration object. 938 my $Epoch = $StartDateTimeObject->epoch(); 939 $Epoch += $SecondsToAdd; 940 941 $StartDateTimeObject = DateTime->from_epoch( 942 epoch => $Epoch, 943 time_zone => $TimeZone, 944 locale => $Self->{Locale}, 945 ); 946 947 $StartTime = $Epoch; 948 } 949 950 # Set values for delta 951 my $RemainingWorkingTime = $WorkingTime; 952 953 $Delta->{Hours} = int $RemainingWorkingTime / ( 60 * 60 ); 954 $RemainingWorkingTime -= $Delta->{Hours} * 60 * 60; 955 956 $Delta->{Minutes} = int $RemainingWorkingTime / 60; 957 $RemainingWorkingTime -= $Delta->{Minutes} * 60; 958 959 $Delta->{Seconds} = $RemainingWorkingTime; 960 $RemainingWorkingTime = 0; 961 962 $Delta->{AbsoluteSeconds} = $WorkingTime; 963 964 return $Delta; 965 } 966 967 # 968 # Calculate delta for "normal" date/time 969 # 970 my $DeltaDuration = $Self->{CPANDateTimeObject}->subtract_datetime( 971 $Param{DateTimeObject}->{CPANDateTimeObject} 972 ); 973 974 $Delta->{Years} = $DeltaDuration->years(); 975 $Delta->{Months} = $DeltaDuration->months(); 976 $Delta->{Weeks} = $DeltaDuration->weeks(); 977 $Delta->{Days} = $DeltaDuration->days(); 978 $Delta->{Hours} = $DeltaDuration->hours(); 979 $Delta->{Minutes} = $DeltaDuration->minutes(); 980 $Delta->{Seconds} = $DeltaDuration->seconds(); 981 982 # Absolute seconds 983 $DeltaDuration = $Self->{CPANDateTimeObject}->subtract_datetime_absolute( 984 $Param{DateTimeObject}->{CPANDateTimeObject} 985 ); 986 987 $Delta->{AbsoluteSeconds} = $DeltaDuration->seconds(); 988 989 return $Delta; 990} 991 992=head2 Compare() 993 994Compares dates and returns a value suitable for using Perl's sort function (-1, 0, 1). 995 996 my $Result = $DateTimeObject->Compare( DateTimeObject => $AnotherDateTimeObject ); 997 998You can also use this as a function for Perl's sort: 999 1000 my @SortedDateTimeObjects = sort { $a->Compare( DateTimeObject => $b ); } @UnsortedDateTimeObjects: 1001 1002Returns: 1003 1004 $Result = -1; # if date/time of this object < date/time of given object 1005 $Result = 0; # if date/time are equal 1006 $Result = 1: # if date/time of this object > date/time of given object 1007 1008=cut 1009 1010sub Compare { 1011 my ( $Self, %Param ) = @_; 1012 1013 if ( 1014 !defined $Param{DateTimeObject} 1015 || ref $Param{DateTimeObject} ne ref $Self 1016 ) 1017 { 1018 $Kernel::OM->Get('Kernel::System::Log')->Log( 1019 'Priority' => 'Error', 1020 'Message' => "Missing or invalid parameter DateTimeObject.", 1021 ); 1022 return; 1023 } 1024 1025 my $Result; 1026 eval { 1027 $Result = DateTime->compare( 1028 $Self->{CPANDateTimeObject}, 1029 $Param{DateTimeObject}->{CPANDateTimeObject} 1030 ); 1031 }; 1032 1033 return $Result; 1034} 1035 1036=head2 ToTimeZone() 1037 1038Converts the date and time of this object to the given time zone. 1039 1040 my $Success = $DateTimeObject->ToTimeZone( 1041 TimeZone => 'Europe/Berlin', 1042 ); 1043 1044Returns: 1045 1046 $Success = 1; # success, or false otherwise. 1047 1048=cut 1049 1050sub ToTimeZone { 1051 my ( $Self, %Param ) = @_; 1052 1053 for my $RequiredParam (qw( TimeZone )) { 1054 if ( !defined $Param{$RequiredParam} ) { 1055 $Kernel::OM->Get('Kernel::System::Log')->Log( 1056 'Priority' => 'Error', 1057 'Message' => "Missing parameter $RequiredParam.", 1058 ); 1059 return; 1060 } 1061 } 1062 1063 eval { 1064 $Self->{CPANDateTimeObject}->set_time_zone( $Param{TimeZone} ); 1065 }; 1066 1067 return if $@; 1068 1069 return 1; 1070} 1071 1072=head2 ToOTRSTimeZone() 1073 1074Converts the date and time of this object to the data storage time zone. 1075 1076 my $Success = $DateTimeObject->ToOTRSTimeZone(); 1077 1078Returns: 1079 1080 $Success = 1; # success, or false otherwise. 1081 1082=cut 1083 1084sub ToOTRSTimeZone { 1085 my ( $Self, %Param ) = @_; 1086 1087 return $Self->ToTimeZone( TimeZone => $Self->OTRSTimeZoneGet() ); 1088} 1089 1090=head2 Validate() 1091 1092Checks if given date, time and time zone would result in a valid date. 1093 1094 my $IsValid = $DateTimeObject->Validate( 1095 Year => 2016, 1096 Month => 1, 1097 Day => 22, 1098 Hour => 16, 1099 Minute => 35, 1100 Second => 59, 1101 TimeZone => 'Europe/Berlin', 1102 ); 1103 1104Returns: 1105 1106 $IsValid = 1; # if date/time is valid, or false otherwise. 1107 1108=cut 1109 1110sub Validate { 1111 my ( $Self, %Param ) = @_; 1112 1113 my @DateTimeParams = qw ( Year Month Day Hour Minute Second TimeZone ); 1114 for my $RequiredDateTimeParam (@DateTimeParams) { 1115 if ( !defined $Param{$RequiredDateTimeParam} ) { 1116 $Kernel::OM->Get('Kernel::System::Log')->Log( 1117 'Priority' => 'Error', 1118 'Message' => "Missing parameter $RequiredDateTimeParam.", 1119 ); 1120 return; 1121 } 1122 } 1123 1124 my $DateTimeObject = $Self->_CPANDateTimeObjectCreate(%Param); 1125 return if !$DateTimeObject; 1126 1127 return 1; 1128} 1129 1130=head2 Format() 1131 1132Returns the date/time as string formatted according to format given. 1133 1134See L<http://search.cpan.org/~drolsky/DateTime-1.21/lib/DateTime.pm#strftime_Patterns> for supported formats. 1135 1136Short overview of essential formatting options: 1137 1138 %Y or %{year}: four digit year 1139 1140 %m: month with leading zero 1141 %{month}: month without leading zero 1142 1143 %d: day of month with leading zero 1144 %{day}: day of month without leading zero 1145 1146 %H: 24 hour with leading zero 1147 %{hour}: 24 hour without leading zero 1148 1149 %l: 12 hour with leading zero 1150 %{hour_12}: 12 hour without leading zero 1151 1152 %M: minute with leading zero 1153 %{minute}: minute without leading zero 1154 1155 %S: second with leading zero 1156 %{second}: second without leading zero 1157 1158 %{time_zone_long_name}: Time zone, e. g. 'Europe/Berlin' 1159 1160 %{epoch}: Seconds since the epoch (OS specific) 1161 %{offset}: Offset in seconds to GMT/UTC 1162 1163 my $DateTimeString = $DateTimeObject->Format( Format => '%Y-%m-%d %H:%M:%S' ); 1164 1165Returns: 1166 1167 my $String = '2016-01-22 18:07:23'; 1168 1169=cut 1170 1171sub Format { 1172 my ( $Self, %Param ) = @_; 1173 1174 for my $RequiredParam (qw( Format )) { 1175 if ( !defined $Param{$RequiredParam} ) { 1176 $Kernel::OM->Get('Kernel::System::Log')->Log( 1177 'Priority' => 'Error', 1178 'Message' => "Missing parameter $RequiredParam.", 1179 ); 1180 1181 return; 1182 } 1183 } 1184 1185 return $Self->{CPANDateTimeObject}->strftime( $Param{Format} ); 1186} 1187 1188=head2 ToEpoch() 1189 1190Returns date/time as seconds since the epoch. 1191 1192 my $Epoch = $DateTimeObject->ToEpoch(); 1193 1194Returns e. g.: 1195 1196 my $Epoch = 1454420017; 1197 1198=cut 1199 1200sub ToEpoch { 1201 my ( $Self, %Param ) = @_; 1202 1203 return $Self->{CPANDateTimeObject}->epoch(); 1204} 1205 1206=head2 ToString() 1207 1208Returns date/time as string. 1209 1210 my $DateTimeString = $DateTimeObject->ToString(); 1211 1212Returns e. g.: 1213 1214 $DateTimeString = '2016-01-31 14:05:45' 1215 1216=cut 1217 1218sub ToString { 1219 my ( $Self, %Param ) = @_; 1220 1221 return $Self->Format( Format => '%Y-%m-%d %H:%M:%S' ); 1222} 1223 1224=head2 ToEmailTimeStamp() 1225 1226Returns the date/time of this object as time stamp in RFC 2822 format to be used in email headers. 1227 1228 my $MailTimeStamp = $DateTimeObject->ToEmailTimeStamp(); 1229 1230 # Typical usage: 1231 # You want to have the date/time of OTRS + its UTC offset, so: 1232 my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime'); 1233 my $MailTimeStamp = $DateTimeObject->ToEmailTimeStamp(); 1234 1235 # If you already have a DateTime object, possibly in another time zone: 1236 $DateTimeObject->ToOTRSTimeZone(); 1237 my $MailTimeStamp = $DateTimeObject->ToEmailTimeStamp(); 1238 1239Returns: 1240 1241 my $String = 'Wed, 2 Sep 2014 16:30:57 +0200'; 1242 1243=cut 1244 1245sub ToEmailTimeStamp { 1246 my ( $Self, %Param ) = @_; 1247 1248 # According to RFC 2822, section 3.3 1249 1250 # The date and time-of-day SHOULD express local time. 1251 # 1252 # The zone specifies the offset from Coordinated Universal Time (UTC, 1253 # formerly referred to as "Greenwich Mean Time") that the date and 1254 # time-of-day represent. The "+" or "-" indicates whether the 1255 # time-of-day is ahead of (i.e., east of) or behind (i.e., west of) 1256 # Universal Time. The first two digits indicate the number of hours 1257 # difference from Universal Time, and the last two digits indicate the 1258 # number of minutes difference from Universal Time. (Hence, +hhmm 1259 # means +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) 1260 # minutes). The form "+0000" SHOULD be used to indicate a time zone at 1261 # Universal Time. Though "-0000" also indicates Universal Time, it is 1262 # used to indicate that the time was generated on a system that may be 1263 # in a local time zone other than Universal Time and therefore 1264 # indicates that the date-time contains no information about the local 1265 # time zone. 1266 1267 my $EmailTimeStamp = $Self->Format( 1268 Format => '%a, %{day} %b %Y %H:%M:%S %z', 1269 ); 1270 1271 return $EmailTimeStamp; 1272} 1273 1274=head2 ToCTimeString() 1275 1276Returns date and time as ctime string, as for example returned by Perl's C<localtime> and C<gmtime> in scalar context. 1277 1278 my $CTimeString = $DateTimeObject->ToCTimeString(); 1279 1280Returns: 1281 1282 my $String = 'Fri Feb 19 16:07:31 2016'; 1283 1284=cut 1285 1286sub ToCTimeString { 1287 my ( $Self, %Param ) = @_; 1288 1289 my $LocalTimeString = $Self->Format( 1290 Format => '%a %b %{day} %H:%M:%S %Y', 1291 ); 1292 1293 return $LocalTimeString; 1294} 1295 1296=head2 IsVacationDay() 1297 1298Checks if date/time of this object is a vacation day. 1299 1300 my $IsVacationDay = $DateTimeObject->IsVacationDay( 1301 Calendar => 9, # optional, OTRS vacation days otherwise 1302 ); 1303 1304Returns: 1305 1306 my $IsVacationDay = 'some vacation day', # description of vacation day or 0 if no vacation day. 1307 1308=cut 1309 1310sub IsVacationDay { 1311 my ( $Self, %Param ) = @_; 1312 1313 my $OriginalDateTimeValues = $Self->Get(); 1314 1315 # Get configured vacation days 1316 my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); 1317 my $TimeVacationDays = $ConfigObject->Get('TimeVacationDays'); 1318 my $TimeVacationDaysOneTime = $ConfigObject->Get('TimeVacationDaysOneTime'); 1319 if ( $Param{Calendar} ) { 1320 if ( $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} . "Name" ) ) { 1321 $TimeVacationDays = $ConfigObject->Get( "TimeVacationDays::Calendar" . $Param{Calendar} ); 1322 $TimeVacationDaysOneTime = $ConfigObject->Get( 1323 "TimeVacationDaysOneTime::Calendar" . $Param{Calendar} 1324 ); 1325 1326 # Switch to time zone of calendar 1327 my $TimeZone = $ConfigObject->Get( "TimeZone::Calendar" . $Param{Calendar} ) 1328 || $Self->OTRSTimeZoneGet(); 1329 1330 if ( defined $TimeZone ) { 1331 $Self->ToTimeZone( TimeZone => $TimeZone ); 1332 } 1333 } 1334 } 1335 1336 my $DateTimeValues = $Self->Get(); 1337 1338 my $VacationDay = $TimeVacationDays->{ $DateTimeValues->{Month} }->{ $DateTimeValues->{Day} }; 1339 my $VacationDayOneTime = $TimeVacationDaysOneTime->{ $DateTimeValues->{Year} }->{ $DateTimeValues->{Month} } 1340 ->{ $DateTimeValues->{Day} }; 1341 1342 # Switch back to original time zone 1343 $Self->ToTimeZone( TimeZone => $OriginalDateTimeValues->{TimeZone} ); 1344 1345 return $VacationDay if defined $VacationDay; 1346 return $VacationDayOneTime if defined $VacationDayOneTime; 1347 1348 return 0; 1349} 1350 1351=head2 LastDayOfMonthGet() 1352 1353Returns the last day of the month. 1354 1355 $LastDayOfMonth = $DateTimeObject->LastDayOfMonthGet(); 1356 1357Returns: 1358 1359 my $LastDayOfMonth = { 1360 Day => 31, 1361 DayOfWeek => 5, 1362 DayAbbr => 'Fri', 1363 }; 1364 1365=cut 1366 1367sub LastDayOfMonthGet { 1368 my ( $Self, %Param ) = @_; 1369 1370 my $DateTimeValues = $Self->Get(); 1371 1372 my $TempCPANDateTimeObject; 1373 eval { 1374 $TempCPANDateTimeObject = DateTime->last_day_of_month( 1375 year => $DateTimeValues->{Year}, 1376 month => $DateTimeValues->{Month}, 1377 ); 1378 }; 1379 1380 return if !$TempCPANDateTimeObject; 1381 1382 my $Result = { 1383 Day => $TempCPANDateTimeObject->day(), 1384 DayOfWeek => $TempCPANDateTimeObject->day_of_week(), 1385 DayAbbr => $TempCPANDateTimeObject->day_abbr(), 1386 }; 1387 1388 return $Result; 1389} 1390 1391=head2 Clone() 1392 1393Clones the DateTime object. 1394 1395 my $ClonedDateTimeObject = $DateTimeObject->Clone(); 1396 1397=cut 1398 1399sub Clone { 1400 my ( $Self, %Param ) = @_; 1401 1402 return __PACKAGE__->new( 1403 _CPANDateTimeObject => $Self->{CPANDateTimeObject}->clone() 1404 ); 1405} 1406 1407=head2 TimeZoneList() 1408 1409Returns an array ref of available time zones. 1410 1411 my $TimeZones = $DateTimeObject->TimeZoneList(); 1412 1413You can also call this method without an object: 1414 1415 my $TimeZones = Kernel::System::DateTime->TimeZoneList(); 1416 1417Returns: 1418 1419 my $TimeZoneList = [ 1420 # ... 1421 'Europe/Amsterdam', 1422 'Europe/Andorra', 1423 'Europe/Athens', 1424 # ... 1425 ]; 1426 1427=cut 1428 1429sub TimeZoneList { 1430 my @TimeZones = @{ DateTime::TimeZone->all_names() }; 1431 1432 # add missing UTC time zone for certain DateTime versions 1433 my %TimeZones = map { $_ => 1 } @TimeZones; 1434 if ( !exists $TimeZones{UTC} ) { 1435 push @TimeZones, 'UTC'; 1436 } 1437 1438 return \@TimeZones; 1439} 1440 1441=head2 TimeZoneByOffsetList() 1442 1443Returns a list of time zones by offset in hours. Of course, the resulting list depends on the date/time set within this 1444DateTime object. 1445 1446 my %TimeZoneByOffset = $DateTimeObject->TimeZoneByOffsetList(); 1447 1448Returns: 1449 1450 my $TimeZoneByOffsetList = { 1451 # ... 1452 -9 => [ 'America/Adak', 'Pacific/Gambier', ], 1453 # ... 1454 2 => [ 1455 # ... 1456 'Europe/Berlin', 1457 # ... 1458 ], 1459 # ... 1460 8.75 => [ 'Australia/Eucla', ], 1461 # ... 1462 }; 1463 1464=cut 1465 1466sub TimeZoneByOffsetList { 1467 my ( $Self, %Param ) = @_; 1468 1469 my $DateTimeObject = $Self->Clone(); 1470 1471 my $TimeZones = $Self->TimeZoneList(); 1472 1473 my %TimeZoneByOffset; 1474 for my $TimeZone ( sort @{$TimeZones} ) { 1475 $DateTimeObject->ToTimeZone( TimeZone => $TimeZone ); 1476 my $TimeZoneOffset = $DateTimeObject->Format( Format => '%{offset}' ) / 60 / 60; 1477 1478 if ( exists $TimeZoneByOffset{$TimeZoneOffset} ) { 1479 push @{ $TimeZoneByOffset{$TimeZoneOffset} }, $TimeZone; 1480 } 1481 else { 1482 $TimeZoneByOffset{$TimeZoneOffset} = [ $TimeZone, ]; 1483 } 1484 } 1485 1486 return \%TimeZoneByOffset; 1487} 1488 1489=head2 IsTimeZoneValid() 1490 1491Checks if the given time zone is valid. 1492 1493 my $Valid = $DateTimeObject->IsTimeZoneValid( TimeZone => 'Europe/Berlin' ); 1494 1495Returns: 1496 $ValidID = 1; # if given time zone is valid, 0 otherwise. 1497 1498=cut 1499 1500my %ValidTimeZones; # Cache for all instances. 1501 1502sub IsTimeZoneValid { 1503 my ( $Self, %Param ) = @_; 1504 1505 for my $RequiredParam (qw( TimeZone )) { 1506 if ( !defined $Param{$RequiredParam} ) { 1507 $Kernel::OM->Get('Kernel::System::Log')->Log( 1508 'Priority' => 'Error', 1509 'Message' => "Missing parameter $RequiredParam.", 1510 ); 1511 return; 1512 } 1513 } 1514 1515 # allow DateTime internal time zone in 'floating' 1516 return 1 if $Param{TimeZone} eq 'floating'; 1517 1518 if ( !%ValidTimeZones ) { 1519 %ValidTimeZones = map { $_ => 1 } @{ $Self->TimeZoneList() }; 1520 } 1521 1522 return $ValidTimeZones{ $Param{TimeZone} } ? 1 : 0; 1523} 1524 1525=head2 OTRSTimeZoneGet() 1526 1527Returns the time zone set for OTRS. 1528 1529 my $OTRSTimeZone = $DateTimeObject->OTRSTimeZoneGet(); 1530 1531 # You can also call this method without an object: 1532 #my $OTRSTimeZone = Kernel::System::DateTime->OTRSTimeZoneGet(); 1533 1534Returns: 1535 1536 my $OTRSTimeZone = 'Europe/Berlin'; 1537 1538=cut 1539 1540sub OTRSTimeZoneGet { 1541 return $Kernel::OM->Get('Kernel::Config')->Get('OTRSTimeZone') || 'UTC'; 1542} 1543 1544=head2 UserDefaultTimeZoneGet() 1545 1546Returns the time zone set as default in SysConfig UserDefaultTimeZone for newly created users or existing users without 1547time zone setting. 1548 1549 my $UserDefaultTimeZoneGet = $DateTimeObject->UserDefaultTimeZoneGet(); 1550 1551You can also call this method without an object: 1552 1553 my $UserDefaultTimeZoneGet = Kernel::System::DateTime->UserDefaultTimeZoneGet(); 1554 1555Returns: 1556 1557 my $UserDefaultTimeZone = 'Europe/Berlin'; 1558 1559=cut 1560 1561sub UserDefaultTimeZoneGet { 1562 return $Kernel::OM->Get('Kernel::Config')->Get('UserDefaultTimeZone') || 'UTC'; 1563} 1564 1565=head2 SystemTimeZoneGet() 1566 1567Returns the time zone of the system. 1568 1569 my $SystemTimeZone = $DateTimeObject->SystemTimeZoneGet(); 1570 1571You can also call this method without an object: 1572 1573 my $SystemTimeZone = Kernel::System::DateTime->SystemTimeZoneGet(); 1574 1575Returns: 1576 1577 my $SystemTimeZone = 'Europe/Berlin'; 1578 1579=cut 1580 1581sub SystemTimeZoneGet { 1582 return DateTime::TimeZone->new( name => 'local' )->name(); 1583} 1584 1585=begin Internal: 1586 1587=head2 _ToCPANDateTimeParamNames() 1588 1589Maps date/time parameter names expected by the methods of this package to the ones expected by CPAN/Perl DateTime 1590package. 1591 1592 my $DateTimeParams = $DateTimeObject->_ToCPANDateTimeParamNames( 1593 Year => 2016, 1594 Month => 1, 1595 Day => 22, 1596 Hour => 17, 1597 Minute => 20, 1598 Second => 2, 1599 TimeZone => 'Europe/Berlin', 1600 ); 1601 1602Returns: 1603 1604 my $CPANDateTimeParamNames = { 1605 year => 2016, 1606 month => 1, 1607 day => 22, 1608 hour => 17, 1609 minute => 20, 1610 second => 2, 1611 time_zone => 'Europe/Berlin', 1612 }; 1613 1614=cut 1615 1616sub _ToCPANDateTimeParamNames { 1617 my ( $Self, %Param ) = @_; 1618 1619 my %ParamNameMapping = ( 1620 Year => 'year', 1621 Month => 'month', 1622 Day => 'day', 1623 Hour => 'hour', 1624 Minute => 'minute', 1625 Second => 'second', 1626 TimeZone => 'time_zone', 1627 1628 Years => 'years', 1629 Months => 'months', 1630 Weeks => 'weeks', 1631 Days => 'days', 1632 Hours => 'hours', 1633 Minutes => 'minutes', 1634 Seconds => 'seconds', 1635 ); 1636 1637 my $DateTimeParams; 1638 1639 PARAMNAME: 1640 for my $ParamName ( sort keys %ParamNameMapping ) { 1641 next PARAMNAME if !exists $Param{$ParamName}; 1642 1643 $DateTimeParams->{ $ParamNameMapping{$ParamName} } = $Param{$ParamName}; 1644 } 1645 1646 return $DateTimeParams; 1647} 1648 1649=head2 _StringToHash() 1650 1651Parses a date/time string and returns a hash ref. 1652 1653 my $DateTimeHash = $DateTimeObject->_StringToHash( String => '2016-08-14 22:45:00' ); 1654 1655 # Sets second to 0: 1656 my $DateTimeHash = $DateTimeObject->_StringToHash( String => '2016-08-14 22:45' ); 1657 1658 # Sets hour, minute and second to 0: 1659 my $DateTimeHash = $DateTimeObject->_StringToHash( String => '2016-08-14' ); 1660 1661Please see C<L</new()>> for the list of supported string formats. 1662 1663Returns: 1664 1665 my $DateTimeHash = { 1666 Year => 2016, 1667 Month => 8, 1668 Day => 14, 1669 Hour => 22, 1670 Minute => 45, 1671 Second => 0, 1672 }; 1673 1674=cut 1675 1676sub _StringToHash { 1677 my ( $Self, %Param ) = @_; 1678 1679 for my $RequiredParam (qw( String )) { 1680 if ( !defined $Param{$RequiredParam} ) { 1681 $Kernel::OM->Get('Kernel::System::Log')->Log( 1682 'Priority' => 'Error', 1683 'Message' => "Missing parameter $RequiredParam.", 1684 ); 1685 1686 return; 1687 } 1688 } 1689 1690 if ( $Param{String} =~ m{\A(\d{4})-(\d{1,2})-(\d{1,2})(\s(\d{1,2}):(\d{1,2})(:(\d{1,2}))?)?\z} ) { 1691 1692 my $DateTimeHash = { 1693 Year => int $1, 1694 Month => int $2, 1695 Day => int $3, 1696 Hour => defined $5 ? int $5 : 0, 1697 Minute => defined $6 ? int $6 : 0, 1698 Second => defined $8 ? int $8 : 0, 1699 }; 1700 1701 return $DateTimeHash; 1702 } 1703 1704 # Match the following formats: 1705 # - yyyy-mm-ddThh:mm:ss+tt:zz 1706 # - yyyy-mm-ddThh:mm:ss+ttzz 1707 # - yyyy-mm-ddThh:mm:ss-tt:zz 1708 # - yyyy-mm-ddThh:mm:ss-ttzz 1709 # - yyyy-mm-ddThh:mm:ss [timezone] 1710 # - yyyy-mm-ddThh:mm:ss[timezone] 1711 if ( $Param{String} =~ /^\d{4}-\d{1,2}-\d{1,2}T\d{1,2}:\d{1,2}:\d{1,2}(.+)$/i ) { 1712 my ( $Year, $Month, $Day, $Hour, $Minute, $Second, $OffsetOrTZ ) = 1713 ( $Param{String} =~ m/^(\d{4})-(\d{2})-(\d{2})T(\d{1,2}):(\d{1,2}):(\d{1,2})\s*(.+)$/i ); 1714 1715 my $DateTimeHash = { 1716 Year => int $Year, 1717 Month => int $Month, 1718 Day => int $Day, 1719 Hour => int $Hour, 1720 Minute => int $Minute, 1721 Second => int $Second, 1722 }; 1723 1724 # Check if the rest 'OffsetOrTZ' is an offset or timezone. 1725 # If isn't an offset consider it a timezone 1726 if ( $OffsetOrTZ !~ m/(\+|\-)\d{2}:?\d{2}/i ) { 1727 1728 # Make sure the time zone is valid. Otherwise, assume UTC. 1729 if ( !$Self->IsTimeZoneValid( TimeZone => $OffsetOrTZ ) ) { 1730 $OffsetOrTZ = 'UTC'; 1731 } 1732 1733 return { 1734 %{$DateTimeHash}, 1735 TimeZone => $OffsetOrTZ, 1736 }; 1737 } 1738 1739 # It's an offset, get the time in GMT/UTC. 1740 $OffsetOrTZ =~ s/://i; # Remove the ':' 1741 my $DT = DateTime->new( 1742 ( map { lcfirst $_ => $DateTimeHash->{$_} } keys %{$DateTimeHash} ), 1743 time_zone => $OffsetOrTZ, 1744 ); 1745 $DT->set_time_zone('UTC'); 1746 $DT->set_time_zone( $Self->OTRSTimeZoneGet() ); 1747 1748 return { 1749 ( map { ucfirst $_ => $DT->$_() } qw(year month day hour minute second) ) 1750 }; 1751 } 1752 1753 $Kernel::OM->Get('Kernel::System::Log')->Log( 1754 'Priority' => 'Error', 1755 'Message' => "Invalid date/time string $Param{String}.", 1756 ); 1757 1758 return; 1759} 1760 1761=head2 _CPANDateTimeObjectCreate() 1762 1763Creates a CPAN DateTime object which will be stored within this object and used for date/time calculations. 1764 1765 # Create an object with current date and time 1766 # within time zone set in SysConfig OTRSTimeZone: 1767 my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate(); 1768 1769 # Create an object with current date and time 1770 # within a certain time zone: 1771 my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate( 1772 TimeZone => 'Europe/Berlin', 1773 ); 1774 1775 # Create an object with a specific date and time: 1776 my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate( 1777 Year => 2016, 1778 Month => 1, 1779 Day => 22, 1780 Hour => 12, # optional, defaults to 0 1781 Minute => 35, # optional, defaults to 0 1782 Second => 59, # optional, defaults to 0 1783 TimeZone => 'Europe/Berlin', # optional, defaults to setting of SysConfig OTRSTimeZone 1784 ); 1785 1786 # Create an object from an epoch timestamp. These timestamps are always UTC/GMT, 1787 # hence time zone will automatically be set to UTC. 1788 # 1789 # If parameter Epoch is present, all other parameters except TimeZone will be ignored. 1790 my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate( 1791 Epoch => 1453911685, 1792 ); 1793 1794 # Create an object from a date/time string. 1795 # 1796 # If parameter String is given, Year, Month, Day, Hour, Minute and Second will be ignored. Please see C<L</new()>> 1797 # for the list of supported string formats. 1798 my $CPANDateTimeObject = $DateTimeObject->_CPANDateTimeObjectCreate( 1799 String => '2016-08-14 22:45:00', 1800 TimeZone => 'Europe/Berlin', # optional, defaults to setting of SysConfig OTRSTimeZone 1801 ); 1802 1803=cut 1804 1805sub _CPANDateTimeObjectCreate { 1806 my ( $Self, %Param ) = @_; 1807 1808 # Create object from string 1809 if ( defined $Param{String} ) { 1810 my $DateTimeHash = $Self->_StringToHash( String => $Param{String} ); 1811 if ( !IsHashRefWithData($DateTimeHash) ) { 1812 $Kernel::OM->Get('Kernel::System::Log')->Log( 1813 'Priority' => 'Error', 1814 'Message' => "Invalid value for String: $Param{String}.", 1815 ); 1816 1817 return; 1818 } 1819 1820 %Param = ( 1821 TimeZone => $Param{TimeZone}, 1822 %{$DateTimeHash}, 1823 ); 1824 } 1825 1826 my $CPANDateTimeObject; 1827 my $TimeZone = $Param{TimeZone} || $Self->OTRSTimeZoneGet(); 1828 1829 if ( !$Self->IsTimeZoneValid( TimeZone => $TimeZone ) ) { 1830 $Kernel::OM->Get('Kernel::System::Log')->Log( 1831 'Priority' => 'Error', 1832 'Message' => "Invalid value for TimeZone: $TimeZone.", 1833 ); 1834 1835 return; 1836 } 1837 1838 # Create object from epoch 1839 if ( defined $Param{Epoch} ) { 1840 1841 if ( $Param{Epoch} !~ m{\A[+-]?\d+\z}sm ) { 1842 $Kernel::OM->Get('Kernel::System::Log')->Log( 1843 'Priority' => 'Error', 1844 'Message' => "Invalid value for Epoch: $Param{Epoch}.", 1845 ); 1846 1847 return; 1848 } 1849 1850 eval { 1851 $CPANDateTimeObject = DateTime->from_epoch( 1852 epoch => $Param{Epoch}, 1853 time_zone => $TimeZone, 1854 locale => $Self->{Locale}, 1855 ); 1856 }; 1857 1858 return $CPANDateTimeObject; 1859 } 1860 1861 $Param{TimeZone} = $TimeZone; 1862 1863 # Check if date/time params were given, excluding time zone 1864 my $DateTimeParamsGiven = %Param && ( !defined $Param{TimeZone} || keys %Param > 1 ); 1865 1866 # Create object from date/time parameters 1867 if ($DateTimeParamsGiven) { 1868 1869 # Check existence of required params 1870 for my $RequiredParam (qw( Year Month Day )) { 1871 if ( !$Param{$RequiredParam} ) { 1872 $Kernel::OM->Get('Kernel::System::Log')->Log( 1873 'Priority' => 'Error', 1874 'Message' => "Missing parameter $RequiredParam.", 1875 ); 1876 return; 1877 } 1878 } 1879 1880 # Create DateTime object 1881 my $DateTimeParams = $Self->_ToCPANDateTimeParamNames(%Param); 1882 1883 eval { 1884 $CPANDateTimeObject = DateTime->new( 1885 %{$DateTimeParams}, 1886 locale => $Self->{Locale}, 1887 ); 1888 }; 1889 1890 return $CPANDateTimeObject; 1891 } 1892 1893 # Create object with current date/time. 1894 eval { 1895 $CPANDateTimeObject = DateTime->now( 1896 time_zone => $TimeZone, 1897 locale => $Self->{Locale}, 1898 ); 1899 }; 1900 1901 return $CPANDateTimeObject; 1902} 1903 1904=head2 _OpIsNewerThan() 1905 1906Operator overloading for > 1907 1908=cut 1909 1910sub _OpIsNewerThan { 1911 my ( $Self, $OtherDateTimeObject ) = @_; 1912 1913 my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject ); 1914 return if !defined $Result; 1915 1916 $Result = $Result == 1 ? 1 : 0; 1917 1918 return $Result; 1919} 1920 1921=head2 _OpIsOlderThan() 1922 1923Operator overloading for < 1924 1925=cut 1926 1927sub _OpIsOlderThan { 1928 my ( $Self, $OtherDateTimeObject ) = @_; 1929 1930 my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject ); 1931 return if !defined $Result; 1932 1933 $Result = $Result == -1 ? 1 : 0; 1934 1935 return $Result; 1936} 1937 1938=head2 _OpIsNewerThanOrEquals() 1939 1940Operator overloading for >= 1941 1942=cut 1943 1944sub _OpIsNewerThanOrEquals { 1945 my ( $Self, $OtherDateTimeObject ) = @_; 1946 1947 my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject ); 1948 return if !defined $Result; 1949 1950 $Result = $Result >= 0 ? 1 : 0; 1951 1952 return $Result; 1953} 1954 1955=head2 _OpIsOlderThanOrEquals() 1956 1957Operator overloading for <= 1958 1959=cut 1960 1961sub _OpIsOlderThanOrEquals { 1962 my ( $Self, $OtherDateTimeObject ) = @_; 1963 1964 my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject ); 1965 return if !defined $Result; 1966 1967 $Result = $Result <= 0 ? 1 : 0; 1968 1969 return $Result; 1970} 1971 1972=head2 _OpEquals() 1973 1974Operator overloading for == 1975 1976=cut 1977 1978sub _OpEquals { 1979 my ( $Self, $OtherDateTimeObject ) = @_; 1980 1981 my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject ); 1982 return if !defined $Result; 1983 1984 $Result = !$Result ? 1 : 0; 1985 1986 return $Result; 1987} 1988 1989=head2 _OpNotEquals() 1990 1991Operator overloading for != 1992 1993=cut 1994 1995sub _OpNotEquals { 1996 my ( $Self, $OtherDateTimeObject ) = @_; 1997 1998 my $Result = $Self->Compare( DateTimeObject => $OtherDateTimeObject ); 1999 return if !defined $Result; 2000 2001 $Result = $Result != 0 ? 1 : 0; 2002 2003 return $Result; 2004} 2005 20061; 2007 2008=end Internal: 2009 2010=head1 TERMS AND CONDITIONS 2011 2012This software is part of the OTRS project (L<https://otrs.org/>). 2013 2014This software comes with ABSOLUTELY NO WARRANTY. For details, see 2015the enclosed file COPYING for license information (GPL). If you 2016did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>. 2017 2018=cut 2019