]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Utils/DateTime.pm
69fec7ae5e156a42528123e76711573210247deb
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Utils / DateTime.pm
1 package OpenILS::Utils::DateTime;
2
3 use Time::Local;
4 use Errno;
5 use POSIX;
6 use FileHandle;
7 use Digest::MD5 qw(md5 md5_hex md5_base64);
8 use Exporter;
9 use DateTime;
10 use DateTime::Format::ISO8601;
11 use DateTime::TimeZone;
12
13 =head1 NAME
14
15 OpenILS::Utils::DateTime;
16
17 =head1 DESCRIPTION
18
19 This contains several routines for doing date and time calculation. This
20 is derived from the date/time routines from OpenSRF::Utils.
21
22 =head1 VERSION
23
24 =cut
25
26 our $VERSION = 1.000;
27
28 use vars qw/@ISA $AUTOLOAD %EXPORT_TAGS @EXPORT_OK @EXPORT/;
29 push @ISA, 'Exporter';
30
31 %EXPORT_TAGS = (
32         datetime        => [qw(clean_ISO8601 gmtime_ISO8601 interval_to_seconds seconds_to_interval)],
33 );
34 Exporter::export_ok_tags('datetime');  # add aa, cc and dd to @EXPORT_OK
35
36 our $date_parser = DateTime::Format::ISO8601->new;
37
38 =head1 METHODS
39
40
41 =cut
42
43 sub AUTOLOAD {
44         my $self = shift;
45         my $type = ref($self) or return undef;
46
47         my $name = $AUTOLOAD;
48         $name =~ s/.*://;   # strip fully-qualified portion
49
50         if (defined($_[0])) {
51                 return $self->{$name} = shift;
52         }
53         return $self->{$name};
54 }
55
56 =head2 $thing->interval_to_seconds('interval') OR interval_to_seconds('interval')
57
58 =head2 $thing->seconds_to_interval($seconds) OR seconds_to_interval($seconds)
59
60 Returns the number of seconds for any interval passed, or the interval for the seconds.
61 This is the generic version of B<interval> listed below.
62
63 The interval must match the regex I</\s*\+?\s*(\d+)\s*(\w{1})\w*\s*/g>, for example
64 B<2 weeks, 3 d and 1hour + 17 Months> or
65 B<1 year, 5 Months, 2 weeks, 3 days and 1 hour of seconds> meaning 46148400 seconds.
66
67         my $expire_time = time() + $thing->interval_to_seconds('17h 9m');
68
69 The time size indicator may be one of
70
71 =over 2
72
73 =item s[econd[s]]
74
75 for seconds
76
77 =item m[inute[s]]
78
79 for minutes
80
81 =item h[our[s]]
82
83 for hours
84
85 =item d[ay[s]]
86
87 for days
88
89 =item w[eek[s]]
90
91 for weeks
92
93 =item M[onth[s]]
94
95 for months (really (365 * 1d)/12 ... that may get smarter, though)
96
97 =item y[ear[s]]
98
99 for years (this is 365 * 1d)
100
101 =back
102
103 =cut
104 sub interval_to_seconds {
105         my $self = shift;
106         my $interval = shift || $self;
107
108         $interval =~ s/(\d{2}):(\d{2}):(\d{2})/ $1 h $2 min $3 s /go;
109
110         $interval =~ s/and/,/g;
111         $interval =~ s/,/ /g;
112
113         my $amount = 0;
114         while ($interval =~ /\s*([\+-]?)\s*(\d+)\s*(\w+)\s*/g) {
115                 my ($sign, $count, $type) = ($1, $2, $3);
116                 $count = "$sign$count" if ($sign);
117                 $amount += $count if ($type =~ /^s/);
118                 $amount += 60 * $count if ($type =~ /^m(?!o)/oi);
119                 $amount += 60 * 60 * $count if ($type =~ /^h/);
120                 $amount += 60 * 60 * 24 * $count if ($type =~ /^d/oi);
121                 $amount += 60 * 60 * 24 * 7 * $count if ($type =~ /^w/oi);
122                 $amount += ((60 * 60 * 24 * 365)/12) * $count if ($type =~ /^mo/io);
123                 $amount += 60 * 60 * 24 * 365 * $count if ($type =~ /^y/oi);
124         }
125         return $amount;
126 }
127
128 sub seconds_to_interval {
129         my $self = shift;
130         my $interval = shift || $self;
131
132         my $limit = shift || 's';
133         $limit =~ s/^(.)/$1/o;
134
135         my ($y,$ym,$M,$Mm,$w,$wm,$d,$dm,$h,$hm,$m,$mm,$s,$string);
136         my ($year, $month, $week, $day, $hour, $minute, $second) =
137                 ('year','Month','week','day', 'hour', 'minute', 'second');
138
139         if ($y = int($interval / (60 * 60 * 24 * 365))) {
140                 $string = "$y $year". ($y > 1 ? 's' : '');
141                 $ym = $interval % (60 * 60 * 24 * 365);
142         } else {
143                 $ym = $interval;
144         }
145         return $string if ($limit eq 'y');
146
147         if ($M = int($ym / ((60 * 60 * 24 * 365)/12))) {
148                 $string .= ($string ? ', ':'')."$M $month". ($M > 1 ? 's' : '');
149                 $Mm = $ym % ((60 * 60 * 24 * 365)/12);
150         } else {
151                 $Mm = $ym;
152         }
153         return $string if ($limit eq 'M');
154
155         if ($w = int($Mm / 604800)) {
156                 $string .= ($string ? ', ':'')."$w $week". ($w > 1 ? 's' : '');
157                 $wm = $Mm % 604800;
158         } else {
159                 $wm = $Mm;
160         }
161         return $string if ($limit eq 'w');
162
163         if ($d = int($wm / 86400)) {
164                 $string .= ($string ? ', ':'')."$d $day". ($d > 1 ? 's' : '');
165                 $dm = $wm % 86400;
166         } else {
167                 $dm = $wm;
168         }
169         return $string if ($limit eq 'd');
170
171         if ($h = int($dm / 3600)) {
172                 $string .= ($string ? ', ' : '')."$h $hour". ($h > 1 ? 's' : '');
173                 $hm = $dm % 3600;
174         } else {
175                 $hm = $dm;
176         }
177         return $string if ($limit eq 'h');
178
179         if ($m = int($hm / 60)) {
180                 $string .= ($string ? ', ':'')."$m $minute". ($m > 1 ? 's' : '');
181                 $mm = $hm % 60;
182         } else {
183                 $mm = $hm;
184         }
185         return $string if ($limit eq 'm');
186
187         if ($s = int($mm)) {
188                 $string .= ($string ? ', ':'')."$s $second". ($s > 1 ? 's' : '');
189         } else {
190                 $string = "0s" unless ($string);
191         }
192         return $string;
193 }
194
195 sub gmtime_ISO8601 {
196         my $self = shift;
197         my @date = gmtime;
198
199         my $y = $date[5] + 1900;
200         my $M = $date[4] + 1;
201         my $d = $date[3];
202         my $h = $date[2];
203         my $m = $date[1];
204         my $s = $date[0];
205
206         return sprintf('%d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d+00:00', $y, $M, $d, $h, $m, $s);
207 }
208
209 =head2 clean_ISO8601($date_string)
210
211 Given a date string or a date/time string in a variety of ad-hoc
212 formats, returns an ISO8601-formatted date/time string.
213
214 The date portion of the input is expected to consist of a four-digit
215 year, followed by a two-digit month, followed by a two-digit year,
216 with each part optionally separated by a hyphen.  If there is
217 only a date portion, it will be normalized to use hyphens.
218
219 If there is no time portion in the input, "T00:00:00" is appended
220 before the results are returned.
221
222 For example, "20180917" would become "2018-09-17T00:00:00".
223
224 If the input does not have a recognizable date, it is simply
225 returned as is.
226
227 If there is a time portion, it is expected to consist of two-digit
228 hour, minutes, and seconds delimited by colons.  That time is
229 appended to the return with "T" separting the date and time
230 portions.
231
232 If there is an ISO8601-style numeric timezone offset, it is
233 normalized and appended to the return. If there is no timezone
234 offset supplied in the input, the offset of the server's
235 time zone is append to the return. Note that as implied above,
236 if only a date is supplied, the return value does not include a
237 timezone offset.
238
239 For example, for a server running in U.S. Eastern Daylight
240 Savings time, "20180917 08:31:15" would become "2018-09-17T08:31:15-04:00".
241
242 =cut
243
244 sub clean_ISO8601 {
245         my $self = shift;
246         my $date = shift || $self;
247         if ($date =~ /^\s*(\d{4})-?(\d{2})-?(\d{2})/o) {
248                 my $new_date = "$1-$2-$3";
249
250                 if ($date =~/(\d{2}):(\d{2}):(\d{2})/o) {
251                         $new_date .= "T$1:$2:$3";
252
253                         my $z;
254                         if ($date =~ /([-+]{1})([0-9]{1,2})(?::?([0-9]{1,2}))*\s*$/o) {
255                                 $z = sprintf('%s%0.2d%0.2d',$1,$2,$3)
256                         } else {
257                                 $z =  DateTime::TimeZone::offset_as_string(
258                                         DateTime::TimeZone
259                                                 ->new( name => 'local' )
260                                                 ->offset_for_datetime(
261                                                         $date_parser->parse_datetime($new_date)
262                                                 )
263                                 );
264                         }
265
266                         if (length($z) > 3 && index($z, ':') == -1) {
267                                 substr($z,3,0) = ':';
268                                 substr($z,6,0) = ':' if (length($z) > 6);
269                         }
270                 
271                         $new_date .= $z;
272                 } else {
273                         $new_date .= "T00:00:00";
274                 }
275
276                 return $new_date;
277         }
278         return $date;
279 }
280
281 1;