1 package OpenILS::Utils::DateTime;
7 use Digest::MD5 qw(md5 md5_hex md5_base64);
10 use DateTime::Duration;
11 use DateTime::Format::ISO8601;
12 use DateTime::TimeZone;
16 OpenILS::Utils::DateTime;
20 This contains several routines for doing date and time calculation. This
21 is derived from the date/time routines from OpenSRF::Utils.
29 use vars qw/@ISA $AUTOLOAD %EXPORT_TAGS @EXPORT_OK @EXPORT/;
30 push @ISA, 'Exporter';
33 datetime => [qw(clean_ISO8601 gmtime_ISO8601 interval_to_seconds seconds_to_interval)],
35 Exporter::export_ok_tags('datetime'); # add aa, cc and dd to @EXPORT_OK
37 our $date_parser = DateTime::Format::ISO8601->new;
46 my $type = ref($self) or return undef;
49 $name =~ s/.*://; # strip fully-qualified portion
52 return $self->{$name} = shift;
54 return $self->{$name};
57 =head2 $thing->interval_to_seconds('interval', ['context']) OR interval_to_seconds('interval', ['context'])
59 =head2 $thing->seconds_to_interval($seconds) OR seconds_to_interval($seconds)
61 Returns the number of seconds for any interval passed, or the interval for the seconds.
62 This is the generic version of B<interval> listed below.
64 The interval must match the regex I</\s*\+?\s*(\d+)\s*(\w{1})\w*\s*/g>, for example
65 B<2 weeks, 3 d and 1hour + 17 Months> or
66 B<1 year, 5 Months, 2 weeks, 3 days and 1 hour of seconds> meaning 46148400 seconds.
68 my $expire_time = time() + $thing->interval_to_seconds('17h 9m');
70 The time size indicator may be one of
96 for months (really (365 * 1d)/12 ... that may get smarter, though)
100 for years (this is 365 * 1d)
102 Passing in an optional 'context' (DateTime object) will give you the number of seconds for the passed interval *starting from* the given date (e.g. '1 month' from a context of 'February 1' would return the number of seconds needed to get to 'March 1', not the generic calculation of 1/12 of the seconds in a normal year).
107 sub interval_to_seconds {
108 my $class = shift; # throwaway
109 my $interval = ($class eq __PACKAGE__) ? shift : $class;
112 $interval =~ s/(\d{2}):(\d{2}):(\d{2})/ $1 h $2 min $3 s /go;
114 $interval =~ s/and/,/g;
115 $interval =~ s/,/ /g;
119 my $dur = DateTime::Duration->new();
120 while ($interval =~ /\s*([\+-]?)\s*(\d+)\s*(\w+)\s*/g) {
121 my ($sign, $count, $type) = ($1, $2, $3);
122 my $func = ($sign eq '-') ? 'subtract' : 'add';
125 } elsif ($type =~ /^m(?!o)/oi) {
127 } elsif ($type =~ /^h/) {
129 } elsif ($type =~ /^d/oi) {
131 } elsif ($type =~ /^w/oi) {
133 } elsif ($type =~ /^mo/io) {
135 } elsif ($type =~ /^y/oi) {
138 $dur->$func($type => $count);
140 my $later = $context->clone->add_duration($dur);
141 $amount = $later->subtract_datetime_absolute($context)->in_units( 'seconds' );
144 while ($interval =~ /\s*([\+-]?)\s*(\d+)\s*(\w+)\s*/g) {
145 my ($sign, $count, $type) = ($1, $2, $3);
146 $count = "$sign$count" if ($sign);
147 $amount += $count if ($type =~ /^s/);
148 $amount += 60 * $count if ($type =~ /^m(?!o)/oi);
149 $amount += 60 * 60 * $count if ($type =~ /^h/);
150 $amount += 60 * 60 * 24 * $count if ($type =~ /^d/oi);
151 $amount += 60 * 60 * 24 * 7 * $count if ($type =~ /^w/oi);
152 $amount += ((60 * 60 * 24 * 365)/12) * $count if ($type =~ /^mo/io);
153 $amount += 60 * 60 * 24 * 365 * $count if ($type =~ /^y/oi);
159 sub seconds_to_interval {
161 my $interval = shift || $self;
163 my $limit = shift || 's';
164 $limit =~ s/^(.)/$1/o;
166 my ($y,$ym,$M,$Mm,$w,$wm,$d,$dm,$h,$hm,$m,$mm,$s,$string);
167 my ($year, $month, $week, $day, $hour, $minute, $second) =
168 ('year','Month','week','day', 'hour', 'minute', 'second');
170 if ($y = int($interval / (60 * 60 * 24 * 365))) {
171 $string = "$y $year". ($y > 1 ? 's' : '');
172 $ym = $interval % (60 * 60 * 24 * 365);
176 return $string if ($limit eq 'y');
178 if ($M = int($ym / ((60 * 60 * 24 * 365)/12))) {
179 $string .= ($string ? ', ':'')."$M $month". ($M > 1 ? 's' : '');
180 $Mm = $ym % ((60 * 60 * 24 * 365)/12);
184 return $string if ($limit eq 'M');
186 if ($w = int($Mm / 604800)) {
187 $string .= ($string ? ', ':'')."$w $week". ($w > 1 ? 's' : '');
192 return $string if ($limit eq 'w');
194 if ($d = int($wm / 86400)) {
195 $string .= ($string ? ', ':'')."$d $day". ($d > 1 ? 's' : '');
200 return $string if ($limit eq 'd');
202 if ($h = int($dm / 3600)) {
203 $string .= ($string ? ', ' : '')."$h $hour". ($h > 1 ? 's' : '');
208 return $string if ($limit eq 'h');
210 if ($m = int($hm / 60)) {
211 $string .= ($string ? ', ':'')."$m $minute". ($m > 1 ? 's' : '');
216 return $string if ($limit eq 'm');
219 $string .= ($string ? ', ':'')."$s $second". ($s > 1 ? 's' : '');
221 $string = "0s" unless ($string);
230 my $y = $date[5] + 1900;
231 my $M = $date[4] + 1;
237 return sprintf('%d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d+00:00', $y, $M, $d, $h, $m, $s);
240 =head2 clean_ISO8601($date_string)
242 Given a date string or a date/time string in a variety of ad-hoc
243 formats, returns an ISO8601-formatted date/time string.
245 The date portion of the input is expected to consist of a four-digit
246 year, followed by a two-digit month, followed by a two-digit year,
247 with each part optionally separated by a hyphen. If there is
248 only a date portion, it will be normalized to use hyphens.
250 If there is no time portion in the input, "T00:00:00" is appended
251 before the results are returned.
253 For example, "20180917" would become "2018-09-17T00:00:00".
255 If the input does not have a recognizable date, it is simply
258 If there is a time portion, it is expected to consist of two-digit
259 hour, minutes, and seconds delimited by colons. That time is
260 appended to the return with "T" separting the date and time
263 If there is an ISO8601-style numeric timezone offset, it is
264 normalized and appended to the return. If there is no timezone
265 offset supplied in the input, the offset of the server's
266 time zone is append to the return. Note that as implied above,
267 if only a date is supplied, the return value does not include a
270 For example, for a server running in U.S. Eastern Daylight
271 Savings time, "20180917 08:31:15" would become "2018-09-17T08:31:15-04:00".
277 my $date = shift || $self;
278 if ($date =~ /^\s*(\d{4})-?(\d{2})-?(\d{2})/o) {
279 my $new_date = "$1-$2-$3";
281 if ($date =~/(\d{2}):(\d{2}):(\d{2})/o) {
282 $new_date .= "T$1:$2:$3";
285 if ($date =~ /([-+]{1})([0-9]{1,2})(?::?([0-9]{1,2}))*\s*$/o) {
286 $z = sprintf('%s%0.2d%0.2d',$1,$2,$3)
287 } elsif ($date =~ /Z\s*$/) {
290 $z = DateTime::TimeZone::offset_as_string(
292 ->new( name => 'local' )
293 ->offset_for_datetime(
294 $date_parser->parse_datetime($new_date)
299 if (length($z) > 3 && index($z, ':') == -1) {
300 substr($z,3,0) = ':';
301 substr($z,6,0) = ':' if (length($z) > 6);
306 $new_date .= "T00:00:00";