bug 4802 add missing order search help file
[koha.git] / C4 / Dates.pm
blob451e2d34b7bf56f6b7d479c14bae73a23f714f00
1 package C4::Dates;
2 # This file is part of Koha.
4 # Koha is free software; you can redistribute it and/or modify it under the
5 # terms of the GNU General Public License as published by the Free Software
6 # Foundation; either version 2 of the License, or (at your option) any later
7 # version.
9 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along with
14 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
15 # Suite 330, Boston, MA 02111-1307 USA
17 use strict;
18 use warnings;
19 use Carp;
20 use C4::Context;
21 use C4::Debug;
22 use Exporter;
23 use POSIX qw(strftime);
24 use Date::Calc qw(check_date check_time);
25 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
26 use vars qw($debug $cgi_debug);
28 BEGIN {
29 $VERSION = 0.04;
30 @ISA = qw(Exporter);
31 @EXPORT_OK = qw(format_date_in_iso format_date);
34 use vars qw($prefformat);
35 sub _prefformat {
36 unless (defined $prefformat) {
37 $prefformat = C4::Context->preference('dateformat');
39 return $prefformat;
42 our %format_map = (
43 iso => 'yyyy-mm-dd', # plus " HH:MM:SS"
44 metric => 'dd/mm/yyyy', # plus " HH:MM:SS"
45 us => 'mm/dd/yyyy', # plus " HH:MM:SS"
46 sql => 'yyyymmdd HHMMSS',
48 our %posix_map = (
49 iso => '%Y-%m-%d', # or %F, "Full Date"
50 metric => '%d/%m/%Y',
51 us => '%m/%d/%Y',
52 sql => '%Y%m%d %H%M%S',
55 our %dmy_subs = ( # strings to eval (after using regular expression returned by regexp below)
56 # make arrays for POSIX::strftime()
57 iso => '[(($6||0),($5||0),($4||0),$3, $2 - 1, $1 - 1900)]',
58 metric => '[(($6||0),($5||0),($4||0),$1, $2 - 1, $3 - 1900)]',
59 us => '[(($6||0),($5||0),($4||0),$2, $1 - 1, $3 - 1900)]',
60 sql => '[(($6||0),($5||0),($4||0),$3, $2 - 1, $1 - 1900)]',
63 sub regexp ($;$) {
64 my $self = shift;
65 my $delim = qr/:?\:|\/|-/; # "non memory" cluster: no backreference
66 my $format = (@_) ? _recognize_format(shift) : ($self->{'dateformat'} || _prefformat());
68 # Extra layer of checking $self->{'dateformat'}.
69 # Why? Because it is assumed you might want to check regexp against an *instantiated* Dates object as a
70 # way of saying "does this string match *whatever* format that Dates object is?"
72 ($format eq 'sql') and
73 return qr/^(\d{4})(\d{1,2})(\d{1,2})(?:\s{4}(\d{2})(\d{2})(\d{2}))?/;
74 ($format eq 'iso') and
75 return qr/^(\d{4})$delim(\d{1,2})$delim(\d{1,2})(?:(?:\s{1}|T)(\d{2})\:?(\d{2})\:?(\d{2}))?Z?/;
76 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
79 sub dmy_map ($$) {
80 my $self = shift;
81 my $val = shift or return undef;
82 my $dformat = $self->{'dateformat'} or return undef;
83 my $re = $self->regexp();
84 my $xsub = $dmy_subs{$dformat};
85 $debug and print STDERR "xsub: $xsub \n";
86 if ($val =~ /$re/) {
87 my $aref = eval $xsub;
88 _check_date_and_time($aref);
89 return @{$aref};
91 # $debug and
92 carp "Illegal Date '$val' does not match '$dformat' format: " . $self->visual();
93 return 0;
96 sub _check_date_and_time {
97 my $chron_ref = shift;
98 my ($year, $month, $day) = _chron_to_ymd($chron_ref);
99 unless (check_date($year, $month, $day)) {
100 carp "Illegal date specified (year = $year, month = $month, day = $day)";
102 my ($hour, $minute, $second) = _chron_to_hms($chron_ref);
103 unless (check_time($hour, $minute, $second)) {
104 carp "Illegal time specified (hour = $hour, minute = $minute, second = $second)";
108 sub _chron_to_ymd {
109 my $chron_ref = shift;
110 return ($chron_ref->[5] + 1900, $chron_ref->[4] + 1, $chron_ref->[3]);
113 sub _chron_to_hms {
114 my $chron_ref = shift;
115 return ($chron_ref->[2], $chron_ref->[1], $chron_ref->[0]);
118 sub new {
119 my $this = shift;
120 my $class = ref($this) || $this;
121 my $self = {};
122 bless $self, $class;
123 return $self->init(@_);
125 sub init ($;$$) {
126 my $self = shift;
127 my $dformat;
128 $self->{'dateformat'} = $dformat = (scalar(@_) >= 2) ? $_[1] : _prefformat();
129 ($format_map{$dformat}) or croak
130 "Invalid date format '$dformat' from " . ((scalar(@_) >= 2) ? 'argument' : 'system preferences');
131 $self->{'dmy_arrayref'} = [((@_) ? $self->dmy_map(shift) : localtime )] ;
132 $debug and warn "(during init) \@\$self->{'dmy_arrayref'}: " . join(' ',@{$self->{'dmy_arrayref'}}) . "\n";
133 return $self;
135 sub output ($;$) {
136 my $self = shift;
137 my $newformat = (@_) ? _recognize_format(shift) : _prefformat();
138 return (eval {POSIX::strftime($posix_map{$newformat}, @{$self->{'dmy_arrayref'}})} || undef);
140 sub today ($;$) { # NOTE: sets date value to today (and returns it in the requested or current format)
141 my $class = shift;
142 $class = ref($class) || $class;
143 my $format = (@_) ? _recognize_format(shift) : _prefformat();
144 return $class->new()->output($format);
146 sub _recognize_format($) {
147 my $incoming = shift;
148 ($incoming eq 'syspref') and return _prefformat();
149 (scalar grep (/^$incoming$/, keys %format_map) == 1) or croak "The format you asked for ('$incoming') is unrecognized.";
150 return $incoming;
152 sub DHTMLcalendar ($;$) { # interface to posix_map
153 my $class = shift;
154 my $format = (@_) ? shift : _prefformat();
155 return $posix_map{$format};
157 sub format { # get or set dateformat: iso, metric, us, etc.
158 my $self = shift;
159 (@_) or return $self->{'dateformat'};
160 $self->{'dateformat'} = _recognize_format(shift);
162 sub visual {
163 my $self = shift;
164 if (@_) {
165 return $format_map{ _recognize_format(shift) };
167 $self eq __PACKAGE__ and return $format_map{_prefformat()};
168 return $format_map{ eval { $self->{'dateformat'} } || _prefformat()} ;
171 # like the functions from the old C4::Date.pm
172 sub format_date {
173 return __PACKAGE__ -> new(shift,'iso')->output((@_) ? shift : _prefformat());
175 sub format_date_in_iso {
176 return __PACKAGE__ -> new(shift,_prefformat())->output('iso');
180 __END__
182 =head1 C4::Dates.pm - a more object-oriented replacement for Date.pm.
184 The core problem to address is the multiplicity of formats used by different Koha
185 installations around the world. We needed to move away from any hard-coded values at
186 the script level, for example in initial form values or checks for min/max date. The
187 reason is clear when you consider string '07/01/2004'. Depending on the format, it
188 represents July 1st (us), or January 7th (metric), or an invalid value (iso).
190 The formats supported by Koha are:
191 iso - ISO 8601 (extended)
192 us - U.S. standard
193 metric - European standard (slight misnomer, not really decimalized metric)
194 sql - log format, not really for human consumption
196 =head2 ->new([string_date,][date_format])
198 Arguments to new() are optional. If string_date is not supplied, the present system date is
199 used. If date_format is not supplied, the system preference from C4::Context is used.
201 Examples:
203 my $now = C4::Dates->new();
204 my $date1 = C4::Dates->new("09-21-1989","us");
205 my $date2 = C4::Dates->new("19890921 143907","sql");
207 =head2 ->output([date_format])
209 The date value is stored independent of any specific format. Therefore any format can be
210 invoked when displaying it.
212 my $date = C4::Dates->new(); # say today is July 12th, 2010
213 print $date->output("iso"); # prints "2010-07-12"
214 print "\n";
215 print $date->output("metric"); # prints "12-07-2010"
217 However, it is still necessary to know the format of any incoming date value (e.g.,
218 setting the value of an object with new()). Like new(), output() assumes the system preference
219 date format unless otherwise instructed.
221 =head2 ->format([date_format])
223 With no argument, format returns the object's current date_format. Otherwise it attempts to
224 set the object format to the supplied value.
226 Some previously desireable functions are now unnecessary. For example, you might want a
227 method/function to tell you whether or not a Dates.pm object is of the 'iso' type. But you
228 can see by this example that such a test is trivial to accomplish, and not necessary to
229 include in the module:
231 sub is_iso {
232 my $self = shift;
233 return ($self->format() eq "iso");
236 Note: A similar function would need to be included for each format.
238 Instead a dependent script can retrieve the format of the object directly and decide what to
239 do with it from there:
241 my $date = C4::Dates->new();
242 my $format = $date->format();
243 ($format eq "iso") or do_something($date);
245 Or if you just want to print a given value and format, no problem:
247 my $date = C4::Dates->new("1989-09-21", "iso");
248 print $date->output;
250 Alternatively:
252 print C4::Dates->new("1989-09-21", "iso")->output;
254 Or even:
256 print C4::Dates->new("21-09-1989", "metric")->output("iso");
258 =head2 "syspref" -- System Preference(s)
260 Perhaps you want to force data obtained in a known format to display according to the user's system
261 preference, without necessarily knowing what that preference is. For this purpose, you can use the
262 psuedo-format argument "syspref".
264 For example, to print an ISO date (from the database) in the <systempreference> format:
266 my $date = C4::Dates->new($date_from_database,"iso");
267 my $datestring_for_display = $date->output("syspref");
268 print $datestring_for_display;
270 Or even:
272 print C4::Dates->new($date_from_database,"iso")->output("syspref");
274 If you just want to know what the <systempreferece> is, a default Dates object can tell you:
276 C4::Dates->new()->format();
278 =head2 ->DHMTLcalendar([date_format])
280 Returns the format string for DHTML Calendar Display based on date_format.
281 If date_format is not supplied, the return is based on system preference.
283 C4::Dates->DHTMLcalendar(); # e.g., returns "%m/%d/%Y" for 'us' system preference
285 =head3 Error Handling
287 Some error handling is provided in this module, but not all. Requesting an unknown format is a
288 fatal error (because it is programmer error, not user error, typically).
290 Scripts must still perform validation of user input. Attempting to set an invalid value will
291 return 0 or undefined, so a script might check as follows:
293 my $date = C4::Dates->new($input) or deal_with_it("$input didn't work");
295 To validate before creating a new object, use the regexp method of the class:
297 $input =~ C4::Dates->regexp("iso") or deal_with_it("input ($input) invalid as iso format");
298 my $date = C4::Dates->new($input,"iso");
300 More verbose debugging messages are sent in the presence of non-zero $ENV{"DEBUG"}.
302 Notes: if the date in the db is null or empty, interpret null expiration to mean "never expires".
304 =head3 _prefformat()
306 This internal function is used to read the preferred date format
307 from the system preference table. It reads the preference once,
308 then caches it.
310 This replaces using the package variable $prefformat directly, and
311 specifically, doing a call to C4::Context->preference() during
312 module initialization. That way, C4::Dates no longer has a
313 compile-time dependency on having a valid $dbh.
315 =head3 TO DO
317 If the date format is not in <systempreference>, we should send an error back to the user.
318 This kind of check should be centralized somewhere. Probably not here, though.
320 =cut