Bug 7794: follow-up DBRev number + removing UNIQUE index that is now useless
[koha.git] / C4 / Dates.pm
blob50db12e01ad36578b62218f5e9830e9028021c85
1 package C4::Dates;
3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA 02111-1307 USA
18 use strict;
19 use warnings;
20 use Carp;
21 use C4::Context;
22 use C4::Debug;
23 use Exporter;
24 use POSIX qw(strftime);
25 use Date::Calc qw(check_date check_time);
26 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
27 use vars qw($debug $cgi_debug);
29 BEGIN {
30 $VERSION = 0.04;
31 @ISA = qw(Exporter);
32 @EXPORT_OK = qw(format_date_in_iso format_date);
35 use vars qw($prefformat);
37 sub _prefformat {
38 unless ( defined $prefformat ) {
39 $prefformat = C4::Context->preference('dateformat');
41 return $prefformat;
44 sub reset_prefformat { # subroutine to clear the prefformat, called when we change it
45 if (defined $prefformat){
46 $prefformat = C4::Context->preference('dateformat');
50 our %format_map = (
51 iso => 'yyyy-mm-dd', # plus " HH:MM:SS"
52 metric => 'dd/mm/yyyy', # plus " HH:MM:SS"
53 us => 'mm/dd/yyyy', # plus " HH:MM:SS"
54 sql => 'yyyymmdd HHMMSS',
55 rfc822 => 'a, dd b y HH:MM:SS z ',
57 our %posix_map = (
58 iso => '%Y-%m-%d', # or %F, "Full Date"
59 metric => '%d/%m/%Y',
60 us => '%m/%d/%Y',
61 sql => '%Y%m%d %H%M%S',
62 rfc822 => '%a, %d %b %Y %H:%M:%S %z',
65 our %dmy_subs = ( # strings to eval (after using regular expression returned by regexp below)
66 # make arrays for POSIX::strftime()
67 iso => '[(($6||0),($5||0),($4||0),$3, $2 - 1, $1 - 1900)]',
68 metric => '[(($6||0),($5||0),($4||0),$1, $2 - 1, $3 - 1900)]',
69 us => '[(($6||0),($5||0),($4||0),$2, $1 - 1, $3 - 1900)]',
70 sql => '[(($6||0),($5||0),($4||0),$3, $2 - 1, $1 - 1900)]',
71 rfc822 => '[($7, $6, $5, $2, $3, $4 - 1900, $8)]',
74 our @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
76 our @days = qw(Sun Mon Tue Wed Thu Fri Sat);
78 sub regexp ($;$) {
79 my $self = shift;
80 my $delim = qr/:?\:|\/|-/; # "non memory" cluster: no backreference
81 my $format = (@_) ? _recognize_format(shift) : ( $self->{'dateformat'} || _prefformat() );
83 # Extra layer of checking $self->{'dateformat'}.
84 # Why? Because it is assumed you might want to check regexp against an *instantiated* Dates object as a
85 # way of saying "does this string match *whatever* format that Dates object is?"
87 ( $format eq 'sql' )
88 and return qr/^(\d{4})(\d{1,2})(\d{1,2})(?:\s{4}(\d{2})(\d{2})(\d{2}))?/;
89 ( $format eq 'iso' )
90 and return qr/^(\d{4})$delim(\d{1,2})$delim(\d{1,2})(?:(?:\s{1}|T)(\d{2})\:?(\d{2})\:?(\d{2}))?Z?/;
91 ( $format eq 'rfc822' )
92 and return qr/^([a-zA-Z]{3}),\s{1}(\d{1,2})\s{1}([a-zA-Z]{3})\s{1}(\d{4})\s{1}(\d{1,2})\:(\d{1,2})\:(\d{1,2})\s{1}(([\-|\+]\d{4})|([A-Z]{3}))/;
93 return qr/^(\d{1,2})$delim(\d{1,2})$delim(\d{4})(?:\s{1}(\d{1,2})\:?(\d{1,2})\:?(\d{1,2}))?/; # everything else
96 sub dmy_map ($$) {
97 my $self = shift;
98 my $val = shift or return undef;
99 my $dformat = $self->{'dateformat'} or return undef;
100 my $re = $self->regexp();
101 my $xsub = $dmy_subs{$dformat};
102 $debug and print STDERR "xsub: $xsub \n";
103 if ( $val =~ /$re/ ) {
104 my $aref = eval $xsub;
105 if ($dformat eq 'rfc822') {
106 $aref = _abbr_to_numeric($aref, $dformat);
107 pop(@{$aref}); #pop off tz offset because we are not setup to handle tz conversions just yet
109 _check_date_and_time($aref);
110 push @{$aref}, (-1,-1,1); # for some reason unknown to me, setting isdst to -1 or undef causes strftime to fail to return the tz offset which is required in RFC822 format -chris_n
111 return @{$aref};
114 # $debug and
115 carp "Illegal Date '$val' does not match '$dformat' format: " . $self->visual();
116 return 0;
119 sub _abbr_to_numeric {
120 my $aref = shift;
121 my $dformat = shift;
122 my ($month_abbr, $day_abbr) = ($aref->[4], $aref->[3]) if $dformat eq 'rfc822';
124 for( my $i = 0; $i < scalar(@months); $i++ ) {
125 if ( $months[$i] =~ /$month_abbr/ ) {
126 $aref->[4] = $i-1;
127 last;
131 for( my $i = 0; $i < scalar(@days); $i++ ) {
132 if ( $days[$i] =~ /$day_abbr/ ) {
133 $aref->[3] = $i;
134 last;
137 return $aref;
140 sub _check_date_and_time {
141 my $chron_ref = shift;
142 my ( $year, $month, $day ) = _chron_to_ymd($chron_ref);
143 unless ( check_date( $year, $month, $day ) ) {
144 carp "Illegal date specified (year = $year, month = $month, day = $day)";
146 my ( $hour, $minute, $second ) = _chron_to_hms($chron_ref);
147 unless ( check_time( $hour, $minute, $second ) ) {
148 carp "Illegal time specified (hour = $hour, minute = $minute, second = $second)";
152 sub _chron_to_ymd {
153 my $chron_ref = shift;
154 return ( $chron_ref->[5] + 1900, $chron_ref->[4] + 1, $chron_ref->[3] );
157 sub _chron_to_hms {
158 my $chron_ref = shift;
159 return ( $chron_ref->[2], $chron_ref->[1], $chron_ref->[0] );
162 sub new {
163 my $this = shift;
164 my $class = ref($this) || $this;
165 my $self = {};
166 bless $self, $class;
167 return $self->init(@_);
170 sub init ($;$$) {
171 my $self = shift;
172 my $dformat;
173 $self->{'dateformat'} = $dformat = ( scalar(@_) >= 2 ) ? $_[1] : _prefformat();
174 ( $format_map{$dformat} ) or croak "Invalid date format '$dformat' from " . ( ( scalar(@_) >= 2 ) ? 'argument' : 'system preferences' );
175 $self->{'dmy_arrayref'} = [ ( (@_) ? $self->dmy_map(shift) : localtime ) ];
176 if ($debug && $debug > 1) { warn "(during init) \@\$self->{'dmy_arrayref'}: " . join( ' ', @{ $self->{'dmy_arrayref'} } ) . "\n"; }
177 return $self;
180 sub output ($;$) {
181 my $self = shift;
182 my $newformat = (@_) ? _recognize_format(shift) : _prefformat();
183 return ( eval { POSIX::strftime( $posix_map{$newformat}, @{ $self->{'dmy_arrayref'} } ) } || undef );
186 sub today ($;$) { # NOTE: sets date value to today (and returns it in the requested or current format)
187 my $class = shift;
188 $class = ref($class) || $class;
189 my $format = (@_) ? _recognize_format(shift) : _prefformat();
190 return $class->new()->output($format);
193 sub _recognize_format($) {
194 my $incoming = shift;
195 ( $incoming eq 'syspref' ) and return _prefformat();
196 ( scalar grep ( /^$incoming$/, keys %format_map ) == 1 ) or croak "The format you asked for ('$incoming') is unrecognized.";
197 return $incoming;
200 sub DHTMLcalendar ($;$) { # interface to posix_map
201 my $class = shift;
202 my $format = (@_) ? shift : _prefformat();
203 return $posix_map{$format};
206 sub format { # get or set dateformat: iso, metric, us, etc.
207 my $self = shift;
208 (@_) or return $self->{'dateformat'};
209 $self->{'dateformat'} = _recognize_format(shift);
212 sub visual {
213 my $self = shift;
214 if (@_) {
215 return $format_map{ _recognize_format(shift) };
217 $self eq __PACKAGE__ and return $format_map{ _prefformat() };
218 return $format_map{ eval { $self->{'dateformat'} } || _prefformat() };
221 # like the functions from the old C4::Date.pm
222 sub format_date {
223 return __PACKAGE__->new( shift, 'iso' )->output( (@_) ? shift : _prefformat() );
226 sub format_date_in_iso {
227 return __PACKAGE__->new( shift, _prefformat() )->output('iso');
231 __END__
233 =head1 C4::Dates.pm - a more object-oriented replacement for Date.pm.
235 The core problem to address is the multiplicity of formats used by different Koha
236 installations around the world. We needed to move away from any hard-coded values at
237 the script level, for example in initial form values or checks for min/max date. The
238 reason is clear when you consider string '07/01/2004'. Depending on the format, it
239 represents July 1st (us), or January 7th (metric), or an invalid value (iso).
241 The formats supported by Koha are:
242 iso - ISO 8601 (extended)
243 us - U.S. standard
244 metric - European standard (slight misnomer, not really decimalized metric)
245 sql - log format, not really for human consumption
246 rfc822 - Standard for using with RSS feeds, etc.
248 =head2 ->new([string_date,][date_format])
250 Arguments to new() are optional. If string_date is not supplied, the present system date is
251 used. If date_format is not supplied, the system preference from C4::Context is used.
253 Examples:
255 my $now = C4::Dates->new();
256 my $date1 = C4::Dates->new("09-21-1989","us");
257 my $date2 = C4::Dates->new("19890921 143907","sql");
259 =head2 ->output([date_format])
261 The date value is stored independent of any specific format. Therefore any format can be
262 invoked when displaying it.
264 my $date = C4::Dates->new(); # say today is July 12th, 2010
265 print $date->output("iso"); # prints "2010-07-12"
266 print "\n";
267 print $date->output("metric"); # prints "12-07-2010"
269 However, it is still necessary to know the format of any incoming date value (e.g.,
270 setting the value of an object with new()). Like new(), output() assumes the system preference
271 date format unless otherwise instructed.
273 =head2 ->format([date_format])
275 With no argument, format returns the object's current date_format. Otherwise it attempts to
276 set the object format to the supplied value.
278 Some previously desireable functions are now unnecessary. For example, you might want a
279 method/function to tell you whether or not a Dates.pm object is of the 'iso' type. But you
280 can see by this example that such a test is trivial to accomplish, and not necessary to
281 include in the module:
283 sub is_iso {
284 my $self = shift;
285 return ($self->format() eq "iso");
288 Note: A similar function would need to be included for each format.
290 Instead a dependent script can retrieve the format of the object directly and decide what to
291 do with it from there:
293 my $date = C4::Dates->new();
294 my $format = $date->format();
295 ($format eq "iso") or do_something($date);
297 Or if you just want to print a given value and format, no problem:
299 my $date = C4::Dates->new("1989-09-21", "iso");
300 print $date->output;
302 Alternatively:
304 print C4::Dates->new("1989-09-21", "iso")->output;
306 Or even:
308 print C4::Dates->new("21-09-1989", "metric")->output("iso");
310 =head2 "syspref" -- System Preference(s)
312 Perhaps you want to force data obtained in a known format to display according to the user's system
313 preference, without necessarily knowing what that preference is. For this purpose, you can use the
314 psuedo-format argument "syspref".
316 For example, to print an ISO date (from the database) in the <systempreference> format:
318 my $date = C4::Dates->new($date_from_database,"iso");
319 my $datestring_for_display = $date->output("syspref");
320 print $datestring_for_display;
322 Or even:
324 print C4::Dates->new($date_from_database,"iso")->output("syspref");
326 If you just want to know what the <systempreferece> is, a default Dates object can tell you:
328 C4::Dates->new()->format();
330 =head2 ->DHMTLcalendar([date_format])
332 Returns the format string for DHTML Calendar Display based on date_format.
333 If date_format is not supplied, the return is based on system preference.
335 C4::Dates->DHTMLcalendar(); # e.g., returns "%m/%d/%Y" for 'us' system preference
337 =head3 Error Handling
339 Some error handling is provided in this module, but not all. Requesting an unknown format is a
340 fatal error (because it is programmer error, not user error, typically).
342 Scripts must still perform validation of user input. Attempting to set an invalid value will
343 return 0 or undefined, so a script might check as follows:
345 my $date = C4::Dates->new($input) or deal_with_it("$input didn't work");
347 To validate before creating a new object, use the regexp method of the class:
349 $input =~ C4::Dates->regexp("iso") or deal_with_it("input ($input) invalid as iso format");
350 my $date = C4::Dates->new($input,"iso");
352 More verbose debugging messages are sent in the presence of non-zero $ENV{"DEBUG"}.
354 Notes: if the date in the db is null or empty, interpret null expiration to mean "never expires".
356 =head3 _prefformat()
358 This internal function is used to read the preferred date format
359 from the system preference table. It reads the preference once,
360 then caches it.
362 This replaces using the package variable $prefformat directly, and
363 specifically, doing a call to C4::Context->preference() during
364 module initialization. That way, C4::Dates no longer has a
365 compile-time dependency on having a valid $dbh.
367 =head3 TO DO
369 If the date format is not in <systempreference>, we should send an error back to the user.
370 This kind of check should be centralized somewhere. Probably not here, though.
372 =cut