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