Bug 28959: Add virtualshelves.public as a boolean
[koha.git] / Koha / DateUtils.pm
1 package Koha::DateUtils;
2
3 # Copyright (c) 2011 PTFS-Europe Ltd.
4 # This file is part of Koha.
5 #
6 # Koha is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # Koha is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with Koha; if not, see <http://www.gnu.org/licenses>.
18
19 use Modern::Perl;
20 use DateTime;
21 use C4::Context;
22 use Koha::Exceptions;
23
24 use vars qw(@ISA @EXPORT_OK);
25 BEGIN {
26     require Exporter;
27     @ISA = qw(Exporter);
28
29     @EXPORT_OK = qw(
30         dt_from_string
31         output_pref
32         format_sqldatetime
33     );
34 }
35
36 =head1 DateUtils
37
38 Koha::DateUtils - Transitional wrappers to ease use of DateTime
39
40 =head1 DESCRIPTION
41
42 Koha has historically only used dates not datetimes and been content to
43 handle these as strings. It also has confused formatting with actual dates
44 this is a temporary module for wrappers to hide the complexity of switch to DateTime
45
46 =cut
47
48 =head2 dt_ftom_string
49
50 $dt = dt_from_string($date_string, [$format, $timezone ]);
51
52 Passed a date string returns a DateTime object format and timezone default
53 to the system preferences. If the date string is empty DateTime->now is returned
54
55 =cut
56
57 sub dt_from_string {
58     my ( $date_string, $date_format, $tz ) = @_;
59
60     return if $date_string and $date_string =~ m|^0000-0|;
61
62     $tz = C4::Context->tz unless $tz;;
63
64     return DateTime->now( time_zone => $tz ) unless $date_string;
65
66     $date_format = C4::Context->preference('dateformat') unless $date_format;
67
68     if ( ref($date_string) eq 'DateTime' ) {    # already a dt return it
69         return $date_string;
70     }
71
72     my $regex;
73
74     # The fallback format is sql/iso
75     my $fallback_re = qr|
76         (?<year>\d{4})
77         -
78         (?<month>\d{2})
79         -
80         (?<day>\d{2})
81     |xms;
82
83     if ( $date_format eq 'metric' ) {
84         # metric format is "dd/mm/yyyy[ hh:mm:ss]"
85         $regex = qr|
86             (?<day>\d{2})
87             /
88             (?<month>\d{2})
89             /
90             (?<year>\d{4})
91         |xms;
92     }
93     elsif ( $date_format eq 'dmydot' ) {
94         # dmydot format is "dd.mm.yyyy[ hh:mm:ss]"
95         $regex = qr|
96             (?<day>\d{2})
97             .
98             (?<month>\d{2})
99             .
100             (?<year>\d{4})
101         |xms;
102     }
103     elsif ( $date_format eq 'us' ) {
104         # us format is "mm/dd/yyyy[ hh:mm:ss]"
105         $regex = qr|
106             (?<month>\d{2})
107             /
108             (?<day>\d{2})
109             /
110             (?<year>\d{4})
111         |xms;
112     }
113     elsif ( $date_format eq 'rfc3339' ) {
114         $regex = qr/
115             (?<year>\d{4})
116             -
117             (?<month>\d{2})
118             -
119             (?<day>\d{2})
120             ([Tt\s])
121             (?<hour>\d{2})
122             :
123             (?<minute>\d{2})
124             :
125             (?<second>\d{2})
126             (\.\d{1,3})?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))
127         /xms;
128     }
129     elsif ( $date_format eq 'iso' or $date_format eq 'sql' ) {
130         # iso or sql format are yyyy-dd-mm[ hh:mm:ss]"
131         $regex = $fallback_re;
132     }
133     else {
134         die "Invalid dateformat parameter ($date_format)";
135     }
136
137     # Add the faculative time part [hh:mm[:ss]]
138     my $time_re .= qr|
139             (
140                 \s*
141                 (?<hour>\d{2})
142                 :
143                 (?<minute>\d{2})
144                 (
145                     :
146                     (?<second>\d{2})
147                 )?
148                 (
149                     \s
150                     (?<ampm>\w{2})
151                 )?
152             )?
153     |xms;
154     $regex .= $time_re;
155     $fallback_re .= $time_re;
156
157     my %dt_params;
158     my $ampm;
159     if ( $date_string =~ $regex ) {
160         %dt_params = (
161             year   => $+{year},
162             month  => $+{month},
163             day    => $+{day},
164             hour   => $+{hour},
165             minute => $+{minute},
166             second => $+{second},
167         );
168         $ampm = $+{ampm};
169     } elsif ( $date_string =~ $fallback_re ) {
170         %dt_params = (
171             year   => $+{year},
172             month  => $+{month},
173             day    => $+{day},
174             hour   => $+{hour},
175             minute => $+{minute},
176             second => $+{second},
177         );
178         $ampm = $+{ampm};
179     }
180     else {
181         die "The given date ($date_string) does not match the date format ($date_format)";
182     }
183
184     # system allows the 0th of the month
185     $dt_params{day} = '01' if $dt_params{day} eq '00';
186
187     # Set default hh:mm:ss to 00:00:00
188     $dt_params{hour}   = 00 unless defined $dt_params{hour};
189     $dt_params{minute} = 00 unless defined $dt_params{minute};
190     $dt_params{second} = 00 unless defined $dt_params{second};
191
192     if ( $ampm ) {
193         if ( $ampm eq 'AM' ) {
194             $dt_params{hour} = 00 if $dt_params{hour} == 12;
195         } elsif ( $dt_params{hour} != 12 ) { # PM
196             $dt_params{hour} += 12;
197             $dt_params{hour} = 00 if $dt_params{hour} == 24;
198         }
199     }
200
201     my $dt = eval {
202         DateTime->new(
203             %dt_params,
204             # No TZ for dates 'infinite' => see bug 13242
205             ( $dt_params{year} < 9999 ? ( time_zone => $tz->name ) : () ),
206         );
207     };
208     if ($@) {
209         $tz = DateTime::TimeZone->new( name => 'floating' );
210         $dt = DateTime->new(
211             %dt_params,
212             # No TZ for dates 'infinite' => see bug 13242
213             ( $dt_params{year} < 9999 ? ( time_zone => $tz->name ) : () ),
214         );
215     }
216     return $dt;
217 }
218
219 =head2 output_pref
220
221 $date_string = output_pref({ dt => $dt [, dateformat => $date_format, timeformat => $time_format, dateonly => 0|1, as_due_date => 0|1 ] });
222 $date_string = output_pref( $dt );
223
224 Returns a string containing the time & date formatted as per the C4::Context setting,
225 or C<undef> if C<undef> was provided.
226
227 This routine can either be passed a DateTime object or or a hashref.  If it is
228 passed a hashref, the expected keys are a mandatory 'dt' for the DateTime,
229 an optional 'dateformat' to override the dateformat system preference, an
230 optional 'timeformat' to override the TimeFormat system preference value,
231 and an optional 'dateonly' to specify that only the formatted date string
232 should be returned without the time.
233
234 =cut
235
236 sub output_pref {
237     my $params = shift;
238     my ( $dt, $str, $force_pref, $force_time, $dateonly, $as_due_date );
239     if ( ref $params eq 'HASH' ) {
240         $dt         = $params->{dt};
241         $str        = $params->{str};
242         $force_pref = $params->{dateformat};         # if testing we want to override Context
243         $force_time = $params->{timeformat};
244         $dateonly   = $params->{dateonly} || 0;    # if you don't want the hours and minutes
245         $as_due_date = $params->{as_due_date} || 0; # don't display the hours and minutes if eq to 23:59 or 11:59 (depending the TimeFormat value)
246     } else {
247         $dt = $params;
248     }
249
250     Koha::Exceptions::WrongParameter->throw( 'output_pref should not be called with both dt and str parameter' ) if $dt and $str;
251
252     if ( $str ) {
253         local $@;
254         $dt = eval { dt_from_string( $str ) };
255         Koha::Exceptions::WrongParameter->throw("Invalid date '$str' passed to output_pref" ) if $@;
256     }
257
258     return if !defined $dt; # NULL date
259     Koha::Exceptions::WrongParameter->throw( "output_pref is called with '$dt' (ref ". ( ref($dt) ? ref($dt):'SCALAR')."), not a DateTime object")  if ref($dt) ne 'DateTime';
260
261     # FIXME: see bug 13242 => no TZ for dates 'infinite'
262     if ( $dt->ymd !~ /^9999/ ) {
263         my $tz = $dateonly ? DateTime::TimeZone->new(name => 'floating') : C4::Context->tz;
264         $dt->set_time_zone( $tz );
265     }
266
267     my $pref =
268       defined $force_pref ? $force_pref : C4::Context->preference('dateformat');
269
270     my $time_format = $force_time || C4::Context->preference('TimeFormat') || q{};
271     my $time = ( $time_format eq '12hr' ) ? '%I:%M %p' : '%H:%M';
272     my $date;
273     if ( $pref =~ m/^iso/ ) {
274         $date = $dateonly
275           ? $dt->strftime("%Y-%m-%d")
276           : $dt->strftime("%Y-%m-%d $time");
277     }
278     elsif ( $pref =~ m/^rfc3339/ ) {
279         if (!$dateonly) {
280             $date = $dt->strftime('%FT%T%z');
281             substr($date, -2, 0, ':'); # timezone "HHmm" => "HH:mm"
282         }
283         else {
284             $date = $dt->strftime("%Y-%m-%d");
285         }
286     }
287     elsif ( $pref =~ m/^metric/ ) {
288         $date = $dateonly
289           ? $dt->strftime("%d/%m/%Y")
290           : $dt->strftime("%d/%m/%Y $time");
291     }
292     elsif ( $pref =~ m/^dmydot/ ) {
293         $date = $dateonly
294           ? $dt->strftime("%d.%m.%Y")
295           : $dt->strftime("%d.%m.%Y $time");
296     }
297
298     elsif ( $pref =~ m/^us/ ) {
299         $date = $dateonly
300           ? $dt->strftime("%m/%d/%Y")
301           : $dt->strftime("%m/%d/%Y $time");
302     }
303     else {
304         $date = $dateonly
305           ? $dt->strftime("%Y-%m-%d")
306           : $dt->strftime("%Y-%m-%d $time");
307     }
308
309     if ( $as_due_date ) {
310         $time_format eq '12hr'
311             ? $date =~ s| 11:59 PM$||
312             : $date =~ s| 23:59$||;
313     }
314
315     return $date;
316 }
317
318 =head2 format_sqldatetime
319
320 $string = format_sqldatetime( $string_as_returned_from_db );
321
322 a convenience routine for calling dt_from_string and formatting the result
323 with output_pref as it is a frequent activity in scripts
324
325 =cut
326
327 sub format_sqldatetime {
328     my $str        = shift;
329     my $force_pref = shift;    # if testing we want to override Context
330     my $force_time = shift;
331     my $dateonly   = shift;
332
333     if ( defined $str && $str =~ m/^\d{4}-\d{2}-\d{2}/ ) {
334         my $dt = dt_from_string( $str, 'sql' );
335         return q{} unless $dt;
336         $dt->truncate( to => 'minute' );
337         return output_pref({
338             dt => $dt,
339             dateformat => $force_pref,
340             timeformat => $force_time,
341             dateonly => $dateonly
342         });
343     }
344     return q{};
345 }
346
347 1;