1 package OpenILS::Utils::DateTime;
7 use Digest::MD5 qw(md5 md5_hex md5_base64);
10 use DateTime::Format::ISO8601;
11 use DateTime::TimeZone;
15 OpenILS::Utils::DateTime;
19 This contains several routines for doing date and time calculation. This
20 is derived from the date/time routines from OpenSRF::Utils.
28 use vars qw/@ISA $AUTOLOAD %EXPORT_TAGS @EXPORT_OK @EXPORT/;
29 push @ISA, 'Exporter';
32 datetime => [qw(clean_ISO8601 gmtime_ISO8601 interval_to_seconds seconds_to_interval)],
34 Exporter::export_ok_tags('datetime'); # add aa, cc and dd to @EXPORT_OK
36 our $date_parser = DateTime::Format::ISO8601->new;
45 my $type = ref($self) or return undef;
48 $name =~ s/.*://; # strip fully-qualified portion
51 return $self->{$name} = shift;
53 return $self->{$name};
56 =head2 $thing->interval_to_seconds('interval') OR interval_to_seconds('interval')
58 =head2 $thing->seconds_to_interval($seconds) OR seconds_to_interval($seconds)
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.
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.
67 my $expire_time = time() + $thing->interval_to_seconds('17h 9m');
69 The time size indicator may be one of
95 for months (really (365 * 1d)/12 ... that may get smarter, though)
99 for years (this is 365 * 1d)
104 sub interval_to_seconds {
106 my $interval = shift || $self;
108 $interval =~ s/(\d{2}):(\d{2}):(\d{2})/ $1 h $2 min $3 s /go;
110 $interval =~ s/and/,/g;
111 $interval =~ s/,/ /g;
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);
128 sub seconds_to_interval {
130 my $interval = shift || $self;
132 my $limit = shift || 's';
133 $limit =~ s/^(.)/$1/o;
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');
139 if ($y = int($interval / (60 * 60 * 24 * 365))) {
140 $string = "$y $year". ($y > 1 ? 's' : '');
141 $ym = $interval % (60 * 60 * 24 * 365);
145 return $string if ($limit eq 'y');
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);
153 return $string if ($limit eq 'M');
155 if ($w = int($Mm / 604800)) {
156 $string .= ($string ? ', ':'')."$w $week". ($w > 1 ? 's' : '');
161 return $string if ($limit eq 'w');
163 if ($d = int($wm / 86400)) {
164 $string .= ($string ? ', ':'')."$d $day". ($d > 1 ? 's' : '');
169 return $string if ($limit eq 'd');
171 if ($h = int($dm / 3600)) {
172 $string .= ($string ? ', ' : '')."$h $hour". ($h > 1 ? 's' : '');
177 return $string if ($limit eq 'h');
179 if ($m = int($hm / 60)) {
180 $string .= ($string ? ', ':'')."$m $minute". ($m > 1 ? 's' : '');
185 return $string if ($limit eq 'm');
188 $string .= ($string ? ', ':'')."$s $second". ($s > 1 ? 's' : '');
190 $string = "0s" unless ($string);
199 my $y = $date[5] + 1900;
200 my $M = $date[4] + 1;
206 return sprintf('%d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d+00:00', $y, $M, $d, $h, $m, $s);
209 =head2 clean_ISO8601($date_string)
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.
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.
219 If there is no time portion in the input, "T00:00:00" is appended
220 before the results are returned.
222 For example, "20180917" would become "2018-09-17T00:00:00".
224 If the input does not have a recognizable date, it is simply
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
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
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".
246 my $date = shift || $self;
247 if ($date =~ /^\s*(\d{4})-?(\d{2})-?(\d{2})/o) {
248 my $new_date = "$1-$2-$3";
250 if ($date =~/(\d{2}):(\d{2}):(\d{2})/o) {
251 $new_date .= "T$1:$2:$3";
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)
257 $z = DateTime::TimeZone::offset_as_string(
259 ->new( name => 'local' )
260 ->offset_for_datetime(
261 $date_parser->parse_datetime($new_date)
266 if (length($z) > 3 && index($z, ':') == -1) {
267 substr($z,3,0) = ':';
268 substr($z,6,0) = ':' if (length($z) > 6);
273 $new_date .= "T00:00:00";