From 8fa7940b570fbb22c478e31735959ce88fdce30d Mon Sep 17 00:00:00 2001
From: Galen Charlton <gmc@equinoxinitiative.org>
Date: Fri, 21 Sep 2018 11:00:25 -0400
Subject: [PATCH] LP#1552778: copy some date/time utils from OpenSRF

As preparation for subsequent bugfixes, this patch
copies several date/time routines from OpenSRF::Utils
to a new module, OpenILS::Utils::DateTime. Specifically,
the routines copied over are:

* clean_ISO8601() (renaming of the OpenSRF cleanse_ISO8601)
* gmtime_ISO8601()
* interval_to_seconds()
* seconds_to_interval()

This move will allow us to fix bugs in this core routines
without requiring a mandatory OpenSRF upgrade. Furthermore,
with the exception of interval_to_seconds() (and in only one
place), none of those routines are used by OpenSRF itself.

To test
-------
[1] Apply the patch.
[2] Verify that unit tests pass.
[3] Verify that all Perl services start correctly.
[4] Verify that date/time and interval calculations continue
    to work as expected, particularly in circulation.

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
---
 Open-ILS/src/offline/offline.pl                    |  11 +-
 Open-ILS/src/perlmods/MANIFEST                     |   1 +
 .../src/perlmods/lib/OpenILS/Application/Actor.pm  |   2 +-
 .../lib/OpenILS/Application/Actor/ClosedDates.pm   |   4 +-
 .../perlmods/lib/OpenILS/Application/Booking.pm    |   2 +-
 .../src/perlmods/lib/OpenILS/Application/Circ.pm   |  22 +-
 .../lib/OpenILS/Application/Circ/CircCommon.pm     |  22 +-
 .../lib/OpenILS/Application/Circ/Circulate.pm      |  46 ++--
 .../perlmods/lib/OpenILS/Application/Circ/Holds.pm |  22 +-
 .../perlmods/lib/OpenILS/Application/Circ/Money.pm |   4 +-
 .../lib/OpenILS/Application/Circ/NonCat.pm         |   4 +-
 .../lib/OpenILS/Application/Collections.pm         |   2 +-
 .../src/perlmods/lib/OpenILS/Application/Serial.pm |   4 +-
 .../Application/Storage/Driver/Pg/QueryParser.pm   |  12 +-
 .../lib/OpenILS/Application/Storage/Publisher.pm   |   3 +-
 .../Application/Storage/Publisher/action.pm        |  12 +-
 .../OpenILS/Application/Storage/Publisher/actor.pm |  40 ++--
 .../perlmods/lib/OpenILS/Application/Trigger.pm    |   6 +-
 .../lib/OpenILS/Application/Trigger/Reactor.pm     |   4 +-
 .../lib/OpenILS/Application/Trigger/Validator.pm   |   6 +-
 Open-ILS/src/perlmods/lib/OpenILS/SIP.pm           |   4 +-
 Open-ILS/src/perlmods/lib/OpenILS/SIP/Item.pm      |   4 +-
 Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm    |  10 +-
 .../src/perlmods/lib/OpenILS/Utils/DateTime.pm     | 246 +++++++++++++++++++++
 .../src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm |  14 +-
 Open-ILS/src/perlmods/lib/OpenILS/Utils/Penalty.pm |   2 +-
 .../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm    |   2 +-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm   |   2 +-
 Open-ILS/src/perlmods/lib/OpenILS/WWW/Exporter.pm  |   2 +-
 Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm  |   4 +-
 .../src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm  |   2 +-
 .../lib/OpenILS/WWW/TemplateBatchBibUpdate.pm      |   2 +-
 Open-ILS/src/perlmods/live_t/03-overdue_circ.t     |   6 +-
 .../perlmods/live_t/04-overdue_with_closed_dates.t |   6 +-
 Open-ILS/src/perlmods/live_t/05-pay_bills.t        |   2 +-
 .../perlmods/live_t/09-lp1198465_neg_balances.t    |   2 +-
 Open-ILS/src/perlmods/t/14-OpenILS-Utils.t         |  11 +-
 .../src/support-scripts/generate_circ_notices.pl   |  14 +-
 Open-ILS/src/support-scripts/set_pbx_holidays.pl   |   4 +-
 39 files changed, 413 insertions(+), 155 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Utils/DateTime.pm

diff --git a/Open-ILS/src/offline/offline.pl b/Open-ILS/src/offline/offline.pl
index 4bc9e518a7..ae972276f4 100755
--- a/Open-ILS/src/offline/offline.pl
+++ b/Open-ILS/src/offline/offline.pl
@@ -10,7 +10,8 @@ use OpenSRF::EX qw/:try/;
 use Data::Dumper;
 use OpenILS::Utils::Fieldmapper;
 use Digest::MD5 qw/md5_hex/;
-use OpenSRF::Utils qw/:daemon cleanse_ISO8601/;
+use OpenSRF::Utils qw/:daemon/;
+use OpenILS::Utils::DateTime qw/clean_ISO8601/;
 use OpenILS::Utils::OfflineStore;
 use OpenSRF::Utils::SettingsClient;
 use OpenSRF::Utils;
@@ -723,7 +724,7 @@ sub ol_handle_checkout {
             && ! $seen_barcode{$barcode}
         ) {
             $seen_barcode{$barcode} = 1;
-            my $cts = DateTime::Format::ISO8601->parse_datetime( cleanse_ISO8601($c->status_changed_time()) )->epoch();
+            my $cts = DateTime::Format::ISO8601->parse_datetime( clean_ISO8601($c->status_changed_time()) )->epoch();
             my $xts = $command->{timestamp}; # Transaction Time Stamp
             $logger->activity("offline: ol_handle_checkout: considering status_changed_time for barcode=$barcode, cts=$cts, xts=$xts");
 
@@ -786,7 +787,7 @@ sub ol_handle_renew {
             && ! $seen_barcode{$barcode}
         ) {
             $seen_barcode{$barcode} = 1;
-            my $cts = DateTime::Format::ISO8601->parse_datetime( cleanse_ISO8601($c->status_changed_time()) )->epoch();
+            my $cts = DateTime::Format::ISO8601->parse_datetime( clean_ISO8601($c->status_changed_time()) )->epoch();
             my $xts = $command->{timestamp}; # Transaction Time Stamp
             $logger->activity("offline: ol_handle_renew: considering status_changed_time for barcode=$barcode, cts=$cts, xts=$xts");
 
@@ -843,7 +844,7 @@ sub ol_handle_checkin {
             && ! $seen_barcode{$barcode}
         ) {
             $seen_barcode{$barcode} = 1;
-            my $cts = DateTime::Format::ISO8601->parse_datetime( cleanse_ISO8601($c->status_changed_time()) )->epoch();
+            my $cts = DateTime::Format::ISO8601->parse_datetime( clean_ISO8601($c->status_changed_time()) )->epoch();
             my $xts = $command->{timestamp}; # Transaction Time Stamp
             $logger->activity("offline: ol_handle_checkin: considering status_changed_time for barcode=$barcode, cts=$cts, xts=$xts");
 
@@ -982,7 +983,7 @@ sub ol_handle_register {
     # calculate the expire date for the patron based on the profile group
     my ($grp) = grep {$_->id == $actor->profile} @$user_groups;
     if($grp) {
-        my $seconds = OpenSRF::Utils->interval_to_seconds($grp->perm_interval);
+        my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($grp->perm_interval);
         my $expire_date = DateTime->from_epoch(epoch => DateTime->now->epoch + $seconds)->epoch;
 		$logger->debug("offline: setting expire date to $expire_date");
         $actor->expire_date($U->epoch2ISO8601($expire_date));
diff --git a/Open-ILS/src/perlmods/MANIFEST b/Open-ILS/src/perlmods/MANIFEST
index 8473b19ce2..8e7b0f58ac 100644
--- a/Open-ILS/src/perlmods/MANIFEST
+++ b/Open-ILS/src/perlmods/MANIFEST
@@ -136,6 +136,7 @@ lib/OpenILS/Utils/Configure.pm
 lib/OpenILS/Utils/Cronscript.pm
 lib/OpenILS/Utils/Cronscript.pm.in
 lib/OpenILS/Utils/CStoreEditor.pm
+lib/OpenILS/Utils/DateTime.pm
 lib/OpenILS/Utils/Fieldmapper.pm
 lib/OpenILS/Utils/HTTPClient.pm
 lib/OpenILS/Utils/ISBN.pm
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
index 662241cf5a..31e7083e50 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
@@ -16,7 +16,7 @@ use OpenILS::Application::AppUtils;
 use OpenILS::Utils::Fieldmapper;
 use OpenILS::Utils::ModsParser;
 use OpenSRF::Utils::Logger qw/$logger/;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::SettingsClient;
 
 use OpenSRF::Utils::Cache;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
index bc87abeb9c..fc53c1681a 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
@@ -2,7 +2,7 @@ package OpenILS::Application::Actor::ClosedDates;
 use base 'OpenILS::Application';
 use strict; use warnings;
 use OpenSRF::EX qw(:try);
-use OpenSRF::Utils qw(:datetime);
+use OpenILS::Utils::DateTime qw(:datetime);
 use DateTime;
 use DateTime::Format::ISO8601;
 use OpenILS::Utils::CStoreEditor q/:funcs/;
@@ -302,7 +302,7 @@ sub is_probably_emergency_closing {
 
     # First, when is it?
     my $start_seconds = DateTime::Format::ISO8601->parse_datetime(
-        cleanse_ISO8601($date->close_start)
+        clean_ISO8601($date->close_start)
     )->epoch;
 
     # Is it in the past?
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm
index 37270114fa..d7ea2121d8 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm
@@ -7,7 +7,7 @@ use POSIX qw/strftime/;
 use OpenILS::Application;
 use base qw/OpenILS::Application/;
 
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use OpenILS::Utils::Fieldmapper;
 use OpenILS::Application::AppUtils;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
index 2c987d909e..f1fcf1f3db 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
@@ -20,7 +20,7 @@ use DateTime::Format::ISO8601;
 
 use OpenILS::Application::AppUtils;
 
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::AppSession;
 use OpenILS::Utils::ModsParser;
 use OpenILS::Event;
@@ -488,14 +488,14 @@ sub set_circ_claims_returned {
     $circ->stop_fines_time('now') unless $circ->stop_fines_time;
 
     if( $backdate ) {
-        $backdate = cleanse_ISO8601($backdate);
+        $backdate = clean_ISO8601($backdate);
 
-        my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
+        my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
         $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
 
         # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
-        $backdate = cleanse_ISO8601($backdate);
+        $backdate = clean_ISO8601($backdate);
 
         # make it look like the circ stopped at the cliams returned time
         $circ->stop_fines_time($backdate);
@@ -642,8 +642,8 @@ sub post_checkin_backdate_circ_impl {
         $backdate and $circ->checkin_time;
 
     # update the checkin and stop_fines times to reflect the new backdate
-    $circ->stop_fines_time(cleanse_ISO8601($backdate));
-    $circ->checkin_time(cleanse_ISO8601($backdate));
+    $circ->stop_fines_time(clean_ISO8601($backdate));
+    $circ->checkin_time(clean_ISO8601($backdate));
     $e->update_action_circulation($circ) or return $e->die_event;
 
     # now void the overdues "erased" by the back-dating
@@ -680,12 +680,12 @@ sub set_circ_due_date {
         or return $e->die_event;
 
     return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
-    $date = cleanse_ISO8601($date);
+    $date = clean_ISO8601($date);
 
     if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
-        my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
+        my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
-        $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
+        $date = clean_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
     }
 
     $circ->due_date($date);
@@ -743,7 +743,7 @@ sub create_in_house_use {
     }
 
     if( $use_time ne 'now' ) {
-        $use_time = cleanse_ISO8601($use_time);
+        $use_time = clean_ISO8601($use_time);
         $logger->debug("in_house_use setting use time to $use_time");
     }
 
@@ -1590,7 +1590,7 @@ sub mark_item_missing_pieces {
 
                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
                 my $due_date = DateTime->now(time_zone => 'local');
-                $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
+                $co_params->{'due_date'} = clean_ISO8601( $due_date->strftime('%FT%T%z') );
             } else {
                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
             }
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm
index 879644f189..3b6bfe9310 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm
@@ -3,7 +3,7 @@ use strict; use warnings;
 use DateTime;
 use DateTime::Format::ISO8601;
 use OpenILS::Application::AppUtils;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenILS::Event;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenILS::Utils::CStoreEditor q/:funcs/;
@@ -50,11 +50,11 @@ sub void_or_zero_overdues {
         # turn it into an interval that interval_to_seconds can parse
         my $duration = $circ->fine_interval;
         $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
-        my $interval = OpenSRF::Utils->interval_to_seconds($duration);
+        my $interval = OpenILS::Utils::DateTime->interval_to_seconds($duration);
 
-        my $date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($backdate));
-        my $due_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->due_date))->epoch;
-        my $grace_period = extend_grace_period( $class, $circ->circ_lib, $circ->due_date, OpenSRF::Utils->interval_to_seconds($circ->grace_period), $e);
+        my $date = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($backdate));
+        my $due_date = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($circ->due_date))->epoch;
+        my $grace_period = extend_grace_period( $class, $circ->circ_lib, $circ->due_date, OpenILS::Utils::DateTime->interval_to_seconds($circ->grace_period), $e);
         if($date->epoch <= $due_date + $grace_period) {
             $logger->info("backdate $backdate is within grace period, voiding all");
         } else {
@@ -223,7 +223,7 @@ sub extend_grace_period {
     my($class, $circ_lib, $due_date, $grace_period, $e, $h) = @_;
     if ($grace_period >= 86400) { # Only extend grace periods greater than or equal to a full day
         my $parser = DateTime::Format::ISO8601->new;
-        my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $due_date ) );
+        my $due_dt = $parser->parse_datetime( clean_ISO8601( $due_date ) );
         my $due = $due_dt->epoch;
 
         my $grace_extend = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend');
@@ -293,7 +293,7 @@ sub extend_grace_period {
                         if ($cl and @$cl) {
                             $closed = 1;
                             foreach (@$cl) {
-                                my $cl_dt = $parser->parse_datetime( cleanse_ISO8601( $_->close_end ) );
+                                my $cl_dt = $parser->parse_datetime( clean_ISO8601( $_->close_end ) );
                                 while ($due_dt <= $cl_dt) {
                                     $due_dt->add( seconds => 86400 );
                                     $new_grace_period += 86400;
@@ -493,7 +493,7 @@ sub generate_fines {
             # each (ils) transaction is processed in its own (db) transaction
             $e->xact_begin if $commit;
 
-            my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $c->$due_date_method ) );
+            my $due_dt = $parser->parse_datetime( clean_ISO8601( $c->$due_date_method ) );
     
             my $due = $due_dt->epoch;
             my $now = time;
@@ -541,8 +541,8 @@ sub generate_fines {
     
             my $last_fine;
             if ($fine) {
-                $conn->respond( "Last billing time: ".$fine->billing_ts." (clensed format: ".cleanse_ISO8601( $fine->billing_ts ).")") if $conn;
-                $last_fine = $parser->parse_datetime( cleanse_ISO8601( $fine->billing_ts ) )->epoch;
+                $conn->respond( "Last billing time: ".$fine->billing_ts." (clensed format: ".clean_ISO8601( $fine->billing_ts ).")") if $conn;
+                $last_fine = $parser->parse_datetime( clean_ISO8601( $fine->billing_ts ) )->epoch;
             } else {
                 $logger->info( "Potential first billing for circ ".$c->id );
                 $last_fine = $due;
@@ -1051,7 +1051,7 @@ sub _has_refundable_payments {
 
     if ($last_payment->[0]) {
         my $interval_secs = interval_to_seconds($interval);
-        my $payment_ts = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($last_payment->[0]->payment_ts))->epoch;
+        my $payment_ts = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($last_payment->[0]->payment_ts))->epoch;
         my $now = time;
         return 1 if ($payment_ts + $interval_secs >= $now);
     }
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
index 6f5433a836..bf5dd0eabf 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
@@ -391,7 +391,7 @@ use OpenSRF::Utils::Cache;
 use Digest::MD5 qw(md5_hex);
 use DateTime::Format::ISO8601;
 use OpenILS::Utils::PermitHold;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::SettingsClient;
 use OpenILS::Application::Circ::Holds;
 use OpenILS::Application::Circ::Transit;
@@ -1021,7 +1021,7 @@ sub mk_env {
             unless $U->is_true($patron->card->active);
     
         my $expire = DateTime::Format::ISO8601->new->parse_datetime(
-            cleanse_ISO8601($patron->expire_date));
+            clean_ISO8601($patron->expire_date));
     
         $self->bail_on_events(OpenILS::Event->new('PATRON_ACCOUNT_EXPIRED'))
             if( CORE::time > $expire->epoch ) ;
@@ -1200,8 +1200,8 @@ sub do_copy_checks {
                 );
 
                 if($auto_renew_intvl) {
-                    my $intvl_seconds = OpenSRF::Utils->interval_to_seconds($auto_renew_intvl);
-                    my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($old_circ->xact_start) );
+                    my $intvl_seconds = OpenILS::Utils::DateTime->interval_to_seconds($auto_renew_intvl);
+                    my $checkout_time = DateTime::Format::ISO8601->new->parse_datetime( clean_ISO8601($old_circ->xact_start) );
 
                     if(DateTime->now > $checkout_time->add(seconds => $intvl_seconds)) {
                         $payload->{auto_renew} = 1;
@@ -2167,7 +2167,7 @@ sub build_checkout_circ_object {
     # if the user provided an overiding checkout time,
     # (e.g. the checkout really happened several hours ago), then
     # we apply that here.  Does this need a perm??
-    $circ->xact_start(cleanse_ISO8601($self->checkout_time))
+    $circ->xact_start(clean_ISO8601($self->checkout_time))
         if $self->checkout_time;
 
     # if a patron is renewing, 'requestor' will be the patron
@@ -2267,7 +2267,7 @@ sub booking_adjusted_due_date {
         return $self->bail_on_events($self->editor->event)
             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
 
-       $circ->due_date(cleanse_ISO8601($self->due_date));
+       $circ->due_date(clean_ISO8601($self->due_date));
 
     } else {
 
@@ -2297,14 +2297,14 @@ sub booking_adjusted_due_date {
         return $self->bail_on_events($bookings) if ref($bookings) eq 'HASH';
         
         my $dt_parser = DateTime::Format::ISO8601->new;
-        my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($circ->due_date) );
+        my $due_date = $dt_parser->parse_datetime( clean_ISO8601($circ->due_date) );
 
         for my $bid (@$bookings) {
 
             my $booking = $self->editor->retrieve_booking_reservation( $bid );
 
-            my $booking_start = $dt_parser->parse_datetime( cleanse_ISO8601($booking->start_time) );
-            my $booking_end = $dt_parser->parse_datetime( cleanse_ISO8601($booking->end_time) );
+            my $booking_start = $dt_parser->parse_datetime( clean_ISO8601($booking->start_time) );
+            my $booking_end = $dt_parser->parse_datetime( clean_ISO8601($booking->end_time) );
 
             return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
                 if ($booking_start < DateTime->now);
@@ -2325,7 +2325,7 @@ sub booking_adjusted_due_date {
             $new_circ_duration++ if $new_circ_duration % 86400 == 0;
             $circ->duration("$new_circ_duration seconds");
 
-            $circ->due_date(cleanse_ISO8601($due_date->strftime('%FT%T%z')));
+            $circ->due_date(clean_ISO8601($due_date->strftime('%FT%T%z')));
             $changed = 1;
         }
 
@@ -2347,7 +2347,7 @@ sub apply_modified_due_date {
         return $self->bail_on_events($self->editor->event)
             unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
 
-      $circ->due_date(cleanse_ISO8601($self->due_date));
+      $circ->due_date(clean_ISO8601($self->due_date));
 
    } else {
 
@@ -2393,13 +2393,13 @@ sub create_due_date {
 
     # for now, use the server timezone.  TODO: use workstation org timezone
     my $due_date = DateTime->now(time_zone => 'local');
-    $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) if $start_time;
+    $due_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) if $start_time;
 
     # add the circ duration
-    $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($duration));
+    $due_date->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($duration));
 
     if($date_ceiling) {
-        my $cdate = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date_ceiling));
+        my $cdate = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($date_ceiling));
         if ($cdate > DateTime->now and ($cdate < $due_date or $U->is_true( $force_date ))) {
             $logger->info("circulator: overriding due date with date ceiling: $date_ceiling");
             $due_date = $cdate;
@@ -2480,7 +2480,7 @@ sub checkout_noncat {
 
    my $lib      = $self->noncat_circ_lib || $self->circ_lib;
    my $count    = $self->noncat_count || 1;
-   my $cotime   = cleanse_ISO8601($self->checkout_time) || "";
+   my $cotime   = clean_ISO8601($self->checkout_time) || "";
 
    $logger->info("circulator: circ creating $count noncat circs with checkout time $cotime");
 
@@ -2525,8 +2525,8 @@ sub check_transit_checkin_interval {
     # transit from X to X for whatever reason has no min interval
     return if $self->transit->source == $self->transit->dest;
 
-    my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
-    my $t_start = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->transit->source_send_time));
+    my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($interval);
+    my $t_start = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($self->transit->source_send_time));
     my $horizon = $t_start->add(seconds => $seconds);
 
     # See if we are still within the transit checkin forbidden range
@@ -3568,9 +3568,9 @@ sub handle_fines {
         # If we have a grace period
         if($obj->can('grace_period')) {
             # Parse out the due date
-            my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
+            my $due_date = $dt_parser->parse_datetime( clean_ISO8601($obj->due_date) );
             # Add the grace period to the due date
-            $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
+            $due_date->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($obj->grace_period));
             # Don't generate fines on circs still in grace period
             $skip_for_grace = $due_date > DateTime->now;
         }
@@ -3800,7 +3800,7 @@ sub checkin_handle_lost_or_longoverdue {
             int($tm[3]), int($tm[4]), int($tm[5]), int($tm[6]));
 
         my $last_chance = 
-            OpenSRF::Utils->interval_to_seconds($max_return) + int($due);
+            OpenILS::Utils::DateTime->interval_to_seconds($max_return) + int($due);
 
         $logger->info("MAX OD: $max_return LAST ACTIVITY: ".
             "$last_activity DUEDATE: ".$circ->due_date." TODAY: $today ".
@@ -3865,8 +3865,8 @@ sub checkin_handle_backdate {
     # not the input.  Do we need to do this?  This certainly interferes with
     # backdating of hourly checkouts, but that is likely a very rare case.
     # ------------------------------------------------------------------
-    my $bd = cleanse_ISO8601($self->backdate);
-    my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($self->circ->due_date));
+    my $bd = clean_ISO8601($self->backdate);
+    my $original_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($self->circ->due_date));
     my $new_date = DateTime::Format::ISO8601->new->parse_datetime($bd);
     $new_date->set_hour($original_date->hour());
     $new_date->set_minute($original_date->minute());
@@ -3877,7 +3877,7 @@ sub checkin_handle_backdate {
         $logger->info("circulator: ignoring future backdate: $new_date");
         delete $self->{backdate};
     } else {
-        $self->backdate(cleanse_ISO8601($new_date->datetime()));
+        $self->backdate(clean_ISO8601($new_date->datetime()));
     }
 
     return undef;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index 457b8aa17c..dcbe7dda4f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -33,7 +33,7 @@ use OpenILS::Application::Circ::Transit;
 use OpenILS::Application::Actor::Friends;
 use DateTime;
 use DateTime::Format::ISO8601;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use Digest::MD5 qw(md5_hex);
 use OpenSRF::Utils::Cache;
 use OpenSRF::Utils::JSON;
@@ -269,7 +269,7 @@ sub create_hold {
     my $expire_setting = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON);
     if ($expire_setting) {
         my $expire = DateTime::Format::ISO8601->new->parse_datetime(
-            cleanse_ISO8601($recipient->expire_date));
+            clean_ISO8601($recipient->expire_date));
 
         push( @events, OpenILS::Event->new(
             'PATRON_ACCOUNT_EXPIRED',
@@ -588,7 +588,7 @@ sub retrieve_holds {
         } elsif($cancel_age) { # limit by age
 
             # find all of the canceled holds that were canceled within the configured time frame
-            my $date = DateTime->now->subtract(seconds => OpenSRF::Utils::interval_to_seconds($cancel_age));
+            my $date = DateTime->now->subtract(seconds => OpenILS::Utils::DateTime->interval_to_seconds($cancel_age));
             $date = $U->epoch2ISO8601($date->epoch);
             $holds_query->{where}->{cancel_time} = {'>=' => $date};
         }
@@ -1103,10 +1103,10 @@ sub set_hold_shelf_expire_time {
     return undef unless $shelf_expire;
 
     $start_time = ($start_time) ?
-        DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) :
+        DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
         DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
 
-    my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
+    my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
     my $expire_time = $start_time->add(seconds => $seconds);
 
     # if the shelf expire time overlaps with a pickup lib's
@@ -1117,7 +1117,7 @@ sub set_hold_shelf_expire_time {
 
     if($dateinfo) {
         my $dt_parser = DateTime::Format::ISO8601->new;
-        $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
+        $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
 
         # TODO: enable/disable time bump via setting?
         $expire_time->set(hour => '23', minute => '59', second => '59');
@@ -1305,8 +1305,8 @@ sub _hold_status {
                             dest_recv_time => {'!=' => undef},
                          })->[0];
         my $start_time = ($transit) ? $transit->dest_recv_time : $hold->capture_time;
-        $start_time    = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time));
-        my $end_time   = $start_time->add(seconds => OpenSRF::Utils::interval_to_seconds($hs_wait_interval));
+        $start_time    = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time));
+        my $end_time   = $start_time->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($hs_wait_interval));
 
         return 5 if $end_time > DateTime->now;
         return 4;
@@ -1456,7 +1456,7 @@ sub retrieve_hold_queue_status_impl {
         $user_org, OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL, $e);
     my $min_wait = $U->ou_ancestor_setting_value(
         $user_org, 'circ.holds.min_estimated_wait_interval', $e);
-    $min_wait = OpenSRF::Utils::interval_to_seconds($min_wait || '0 seconds');
+    $min_wait = OpenILS::Utils::DateTime->interval_to_seconds($min_wait || '0 seconds');
     $default_wait ||= '0 seconds';
 
     # Estimated wait time is the average wait time across the set
@@ -1469,7 +1469,7 @@ sub retrieve_hold_queue_status_impl {
     for my $wait_data (@$hold_data) {
         my $count += $wait_data->{count};
         $combined_secs += $count *
-            OpenSRF::Utils::interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
+            OpenILS::Utils::DateTime->interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
         $num_potentials += $count;
     }
 
@@ -4324,7 +4324,7 @@ sub calculate_expire_time
     my $ou = shift;
     my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
     if($interval) {
-        my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
+        my $date = DateTime->now->add(seconds => OpenILS::Utils::DateTime->interval_to_seconds($interval));
         return $U->epoch2ISO8601($date->epoch);
     }
     return undef;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
index 4fa1d1f458..6b84a7892e 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
@@ -32,7 +32,7 @@ use OpenILS::Utils::Penalty;
 use Business::Stripe;
 $Data::Dumper::Indent = 0;
 use OpenILS::Const qw/:const/;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use DateTime::Format::ISO8601;
 my $parser = DateTime::Format::ISO8601->new;
 
@@ -1290,7 +1290,7 @@ __PACKAGE__->register_method(
 sub _to_epoch {
     my $ts = shift @_;
 
-    return $parser->parse_datetime(cleanse_ISO8601($ts))->epoch;
+    return $parser->parse_datetime(clean_ISO8601($ts))->epoch;
 }
 
 my %_statement_sort = (
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/NonCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/NonCat.pm
index 26b85d2cd7..0d576cf5d6 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/NonCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/NonCat.pm
@@ -5,7 +5,7 @@ use OpenSRF::EX qw(:try);
 use Data::Dumper;
 use DateTime;
 use DateTime::Format::ISO8601;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenILS::Application::AppUtils;
 use OpenILS::Utils::Fieldmapper;
@@ -186,7 +186,7 @@ sub noncat_due_date {
     my $otype = $e->retrieve_config_non_cataloged_type($circ->item_type) 
         or return $e->die_event;
 
-    my $duedate = $_dt_parser->parse_datetime( cleanse_ISO8601($circ->circ_time) );
+    my $duedate = $_dt_parser->parse_datetime( clean_ISO8601($circ->circ_time) );
     $duedate = $duedate
         ->add( seconds => interval_to_seconds($otype->circ_duration) )
         ->strftime('%FT%T%z');
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm
index a377029b66..b451cbd8ee 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm
@@ -3,7 +3,7 @@ use strict; use warnings;
 use OpenSRF::EX qw(:try);
 use OpenILS::Application::AppUtils;
 use OpenSRF::Utils::Logger qw(:logger);
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenILS::Application;
 use OpenILS::Utils::Fieldmapper;
 use base 'OpenILS::Application';
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm
index dfe98789b0..fe509e9b93 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm
@@ -43,7 +43,7 @@ use base qw/OpenILS::Application/;
 use OpenILS::Application::AppUtils;
 use OpenILS::Event;
 use OpenSRF::AppSession;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Logger qw/:logger/;
 use OpenILS::Utils::CStoreEditor q/:funcs/;
 use OpenILS::Utils::Fieldmapper;
@@ -1060,7 +1060,7 @@ sub make_prediction_values {
         $logger->debug('make_prediction_values reviving holdings: ' . OpenSRF::Utils::JSON->perl2JSON($predict_from_siss));
         $options->{predict_from} = _revive_holding($predict_from_siss->holding_code, $caption_field, 1); # fresh MFHD Record, so we simply default to 1 for seqno
         if ($fake_chron_needed) {
-            $options->{faked_chron_date} = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($predict_from_siss->date_published));
+            $options->{faked_chron_date} = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($predict_from_siss->date_published));
         }
         $logger->debug('make_prediction_values predicting with options: ' . OpenSRF::Utils::JSON->perl2JSON($options));
         push( @predictions, _generate_issuance_values($mfhd, $options) );
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
index 0d3c4118e6..941085133d 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
@@ -4,7 +4,7 @@ use warnings;
 package OpenILS::Application::Storage::Driver::Pg::QueryParser;
 use OpenILS::Application::Storage::QueryParser;
 use base 'QueryParser';
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::JSON;
 use OpenILS::Application::AppUtils;
 use OpenILS::Utils::CStoreEditor;
@@ -755,7 +755,7 @@ __PACKAGE__->add_search_modifier( 'metabib' );
 package OpenILS::Application::Storage::Driver::Pg::QueryParser::query_plan;
 use base 'QueryParser::query_plan';
 use OpenSRF::Utils::Logger qw($logger);
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use Data::Dumper;
 use OpenILS::Application::AppUtils;
 use OpenILS::Utils::Normalize qw/search_normalize/;
@@ -1671,7 +1671,7 @@ sub flatten {
                             # useless use of filter
                         } else {
                             # "before $cend"
-                            $cend = cleanse_ISO8601($cend);
+                            $cend = clean_ISO8601($cend);
                             $where .= $joiner if $where ne '';
                             $where .= "bre.$datefilter <= \$_$$\$$cend\$_$$\$";
                         }
@@ -1680,14 +1680,14 @@ sub flatten {
                         if ($cstart eq '-infinity') {
                             # useless use of filter
                         } else { # "after $cstart"
-                            $cstart = cleanse_ISO8601($cstart);
+                            $cstart = clean_ISO8601($cstart);
                             $where .= $joiner if $where ne '';
                             $where .= "bre.$datefilter >= \$_$$\$$cstart\$_$$\$";
                         }
                     } else { # both supplied
                         # "between $cstart and $cend"
-                        $cstart = cleanse_ISO8601($cstart);
-                        $cend = cleanse_ISO8601($cend);
+                        $cstart = clean_ISO8601($cstart);
+                        $cend = clean_ISO8601($cend);
                         $where .= $joiner if $where ne '';
                         $where .= "bre.$datefilter BETWEEN \$_$$\$$cstart\$_$$\$ AND \$_$$\$$cend\$_$$\$";
                     }
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher.pm
index db78f30a73..dd90f177cc 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher.pm
@@ -5,6 +5,7 @@ our $VERSION = 1;
 use Digest::MD5 qw/md5_hex/;
 use OpenSRF::EX qw/:try/;
 use OpenSRF::Utils;
+use OpenILS::Utils::DateTime;
 use OpenSRF::Utils::Logger qw/:level/;
 use OpenILS::Utils::Fieldmapper;
 
@@ -112,7 +113,7 @@ sub cachable_wrapper {
         OpenSRF::Utils::Cache->new->put_cache(
             $cache_key =>
             [@res[int($cache_page * $cache_args{cache_page_size}) .. int(($cache_page + 1) * $cache_args{cache_page_size}) ]] =>
-            OpenSRF::Utils->interval_to_seconds( $cache_args{timeout} )
+            OpenILS::Utils::DateTime->interval_to_seconds( $cache_args{timeout} )
         );
     } catch Error with {
         my $e = shift;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
index 3c69f3302c..045c5e5b39 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
@@ -3,7 +3,7 @@ use parent qw/OpenILS::Application::Storage::Publisher/;
 use strict;
 use warnings;
 use OpenSRF::Utils::Logger qw/:level :logger/;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::JSON;
 use OpenSRF::AppSession;
 use OpenSRF::EX qw/:try/;
@@ -1218,7 +1218,7 @@ sub new_hold_copy_targeter {
             $_->delete for (@oldmaps);
 
             if ($hold->expire_time) {
-                my $ex_time = $parser->parse_datetime( cleanse_ISO8601( $hold->expire_time ) );
+                my $ex_time = $parser->parse_datetime( clean_ISO8601( $hold->expire_time ) );
                 if ( DateTime->compare($ex_time, DateTime->now) < 0 ) {
 
                     # cancel cause = un-targeted expiration
@@ -1677,7 +1677,7 @@ sub process_recall {
 
         # Give the user a new due date of either a full recall threshold,
         # or the return interval, whichever is further in the future
-        my $threshold_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->xact_start))->add(seconds => interval_to_seconds($recall_threshold))->iso8601();
+        my $threshold_date = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($circ->xact_start))->add(seconds => interval_to_seconds($recall_threshold))->iso8601();
         if (DateTime->compare(DateTime::Format::ISO8601->parse_datetime($threshold_date), DateTime::Format::ISO8601->parse_datetime($return_date)) == 1) {
             $return_date = $threshold_date;
         }
@@ -1756,7 +1756,7 @@ sub reservation_targeter {
 
             die "OK\n" if (!$bresv or $bresv->capture_time or $bresv->cancel_time);
 
-            my $end_time = $parser->parse_datetime( cleanse_ISO8601( $bresv->end_time ) );
+            my $end_time = $parser->parse_datetime( clean_ISO8601( $bresv->end_time ) );
             if (DateTime->compare($end_time, DateTime->now) < 0) {
 
                 # cancel cause = un-targeted expiration
@@ -1831,8 +1831,8 @@ sub reservation_targeter {
 
                     if (@$circs) {
                         my $due_date = $circs->[0]->due_date;
-                        $due_date = $parser->parse_datetime( cleanse_ISO8601( $due_date ) );
-                        my $start_time = $parser->parse_datetime( cleanse_ISO8601( $bresv->start_time ) );
+                        $due_date = $parser->parse_datetime( clean_ISO8601( $due_date ) );
+                        my $start_time = $parser->parse_datetime( clean_ISO8601( $bresv->start_time ) );
                         if (DateTime->compare($start_time, $due_date) < 0) {
                             $conflicts{$res->id} = $circs->[0]->to_fieldmapper;
                             next;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm
index 686d960ebf..0c1f0a5cad 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm
@@ -2,7 +2,7 @@ package OpenILS::Application::Storage::Publisher::actor;
 use base qw/OpenILS::Application::Storage/;
 use OpenILS::Application::Storage::CDBI::actor;
 use OpenSRF::Utils::Logger qw/:level/;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenILS::Utils::Fieldmapper;
 use OpenSRF::Utils::SettingsClient;
 use OpenILS::Application::AppUtils;
@@ -263,8 +263,8 @@ sub make_closure_spanset {
 
         $spanset = $spanset->union(
             DateTime::Span->new(
-                start => $_dt_parser->parse_datetime(cleanse_ISO8601($c->{close_start})),
-                end   => $_dt_parser->parse_datetime(cleanse_ISO8601($c->{close_end}))
+                start => $_dt_parser->parse_datetime(clean_ISO8601($c->{close_start})),
+                end   => $_dt_parser->parse_datetime(clean_ISO8601($c->{close_end}))
             )
         );
     }
@@ -293,7 +293,7 @@ sub new_org_closed_overlap {
           LIMIT 1
     SQL
 
-    $date = cleanse_ISO8601($date);
+    $date = clean_ISO8601($date);
 
     my $target_date = $_dt_parser->parse_datetime( $date );
     my ($begin, $end) = ($target_date, $target_date);
@@ -312,7 +312,7 @@ sub new_org_closed_overlap {
             $begin->subtract( minutes => 1 );
 
             while ( my $_b = new_org_closed_overlap($self, $client, $ou, $begin->strftime('%FT%T%z'), -1, 1 ) ) {
-                $begin = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
+                $begin = $_dt_parser->parse_datetime( clean_ISO8601($_b->{start}) );
             }
         }
 
@@ -320,7 +320,7 @@ sub new_org_closed_overlap {
             $end->add( minutes => 1 );
 
             while ( my $_a = new_org_closed_overlap($self, $client, $ou, $end->strftime('%FT%T%z'), 1, 1 ) ) {
-                $end = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
+                $end = $_dt_parser->parse_datetime( clean_ISO8601($_a->{end}) );
             }
         }
     }
@@ -337,7 +337,7 @@ sub new_org_closed_overlap {
             $begin->subtract( minutes => 1 );
 
             while ( my $_b = new_org_closed_overlap($self, $client, $ou, $begin->strftime('%FT%T%z'), -1 ) ) {
-                $begin = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
+                $begin = $_dt_parser->parse_datetime( clean_ISO8601($_b->{start}) );
             }
         }
     
@@ -348,7 +348,7 @@ sub new_org_closed_overlap {
 
 
             while ( my $_b = new_org_closed_overlap($self, $client, $ou, $end->strftime('%FT%T%z'), -1 ) ) {
-                $end = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{end}) );
+                $end = $_dt_parser->parse_datetime( clean_ISO8601($_b->{end}) );
             }
         }
     }
@@ -385,23 +385,23 @@ sub org_closed_overlap {
           LIMIT 1
     SQL
 
-    $date = cleanse_ISO8601($date);
+    $date = clean_ISO8601($date);
     my ($begin, $end) = ($date,$date);
 
     my $hoo = actor::org_unit::hours_of_operation->retrieve($ou);
 
     if (my $closure = actor::org_unit::closed_date->db_Main->selectrow_hashref( $sql, {}, $date, $ou )) {
-        $begin = cleanse_ISO8601($closure->{close_start});
-        $end = cleanse_ISO8601($closure->{close_end});
+        $begin = clean_ISO8601($closure->{close_start});
+        $end = clean_ISO8601($closure->{close_end});
 
         if ( $direction <= 0 ) {
             $before = $_dt_parser->parse_datetime( $begin );
             $before->subtract( minutes => 1 );
 
             while ( my $_b = org_closed_overlap($self, $client, $ou, $before->strftime('%FT%T%z'), -1, 1 ) ) {
-                $before = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
+                $before = $_dt_parser->parse_datetime( clean_ISO8601($_b->{start}) );
             }
-            $begin = cleanse_ISO8601($before->strftime('%FT%T%z'));
+            $begin = clean_ISO8601($before->strftime('%FT%T%z'));
         }
 
         if ( $direction >= 0 ) {
@@ -409,9 +409,9 @@ sub org_closed_overlap {
             $after->add( minutes => 1 );
 
             while ( my $_a = org_closed_overlap($self, $client, $ou, $after->strftime('%FT%T%z'), 1, 1 ) ) {
-                $after = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
+                $after = $_dt_parser->parse_datetime( clean_ISO8601($_a->{end}) );
             }
-            $end = cleanse_ISO8601($after->strftime('%FT%T%z'));
+            $end = clean_ISO8601($after->strftime('%FT%T%z'));
         }
     }
 
@@ -425,7 +425,7 @@ sub org_closed_overlap {
 
                 my $count = 1;
                 while ($hoo->$begin_open_meth eq '00:00:00' and $hoo->$begin_close_meth eq '00:00:00') {
-                    $begin = cleanse_ISO8601($_dt_parser->parse_datetime( $begin )->subtract( days => 1)->strftime('%FT%T%z'));
+                    $begin = clean_ISO8601($_dt_parser->parse_datetime( $begin )->subtract( days => 1)->strftime('%FT%T%z'));
                     $begin_dow++;
                     $begin_dow %= 7;
                     $count++;
@@ -438,7 +438,7 @@ sub org_closed_overlap {
                     $before = $_dt_parser->parse_datetime( $begin );
                     $before->subtract( minutes => 1 );
                     while ( my $_b = org_closed_overlap($self, $client, $ou, $before->strftime('%FT%T%z'), -1 ) ) {
-                        $before = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
+                        $before = $_dt_parser->parse_datetime( clean_ISO8601($_b->{start}) );
                     }
                 }
             }
@@ -450,7 +450,7 @@ sub org_closed_overlap {
     
                 $count = 1;
                 while ($hoo->$end_open_meth eq '00:00:00' and $hoo->$end_close_meth eq '00:00:00') {
-                    $end = cleanse_ISO8601($_dt_parser->parse_datetime( $end )->add( days => 1)->strftime('%FT%T%z'));
+                    $end = clean_ISO8601($_dt_parser->parse_datetime( $end )->add( days => 1)->strftime('%FT%T%z'));
                     $end_dow++;
                     $end_dow %= 7;
                     $count++;
@@ -464,9 +464,9 @@ sub org_closed_overlap {
                     $after->add( minutes => 1 );
 
                     while ( my $_a = org_closed_overlap($self, $client, $ou, $after->strftime('%FT%T%z'), 1 ) ) {
-                        $after = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
+                        $after = $_dt_parser->parse_datetime( clean_ISO8601($_a->{end}) );
                     }
-                    $end = cleanse_ISO8601($after->strftime('%FT%T%z'));
+                    $end = clean_ISO8601($after->strftime('%FT%T%z'));
                 }
             }
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger.pm
index 3336dcbc54..9027beb16c 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger.pm
@@ -10,7 +10,7 @@ use OpenSRF::AppSession;
 use OpenSRF::MultiSession;
 use OpenSRF::Utils::SettingsClient;
 use OpenSRF::Utils::Logger qw/$logger/;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 
 use DateTime;
 use DateTime::Format::ISO8601;
@@ -93,7 +93,7 @@ sub create_active_events_for_object {
 
             if (my $dfield = $def->delay_field) {
                 if ($target->$dfield()) {
-                    $date = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($target->$dfield) );
+                    $date = DateTime::Format::ISO8601->new->parse_datetime( clean_ISO8601($target->$dfield) );
                 } else {
                     next;
                 }
@@ -187,7 +187,7 @@ sub create_event_for_object_and_def {
 
             if (my $dfield = $def->delay_field) {
                 if ($target->$dfield()) {
-                    $date = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($target->$dfield) );
+                    $date = DateTime::Format::ISO8601->new->parse_datetime( clean_ISO8601($target->$dfield) );
                 } else {
                     next;
                 }
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm
index f334f66ddd..bf6b67eb2f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor.pm
@@ -6,7 +6,7 @@ use DateTime;
 use DateTime::Format::ISO8601;
 use Unicode::Normalize;
 use XML::LibXML;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenSRF::Utils::JSON;
 use OpenILS::Application::AppUtils;
@@ -43,7 +43,7 @@ $_TT_helpers = {
     # turns a date into something TT can understand
     format_date => sub {
         my $date = shift;
-        $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
+        $date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($date));
         return sprintf(
             "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
             $date->hour,
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm
index c45d1eae5f..ad4e4f4fb6 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm
@@ -2,7 +2,7 @@ package OpenILS::Application::Trigger::Validator;
 use strict; use warnings;
 use DateTime;
 use DateTime::Format::ISO8601;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Logger qw/:logger/;
 use OpenILS::Const qw/:const/;
 use OpenILS::Application::AppUtils;
@@ -47,7 +47,7 @@ sub MinPassiveTargetAge {
         return 0; # no-op false
     }
 
-    my $delay_field_ts = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($target->$delay_field()));
+    my $delay_field_ts = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($target->$delay_field()));
 
     # to get the minimum time that the target must have aged to, add the min age to the delay field
     $delay_field_ts->add( seconds => interval_to_seconds( $env->{params}->{min_target_age} ) );
@@ -69,7 +69,7 @@ sub CircIsOverdue {
         return 0 if (!$self->MinPassiveTargetAge($env));
     }
 
-    my $due_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
+    my $due_date = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
     return 0 if $due_date > DateTime->now;
 
     return 1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/SIP.pm b/Open-ILS/src/perlmods/lib/OpenILS/SIP.pm
index e2639cc535..500efa22bc 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/SIP.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/SIP.pm
@@ -23,7 +23,7 @@ use OpenSRF::AppSession;
 use OpenILS::Utils::Fieldmapper;
 use OpenSRF::Utils::SettingsClient;
 use OpenILS::Application::AppUtils;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use DateTime::Format::ISO8601;
 
 my $U = 'OpenILS::Application::AppUtils';
@@ -147,7 +147,7 @@ sub format_date {
     return "" unless $date;
 
     my $dt = DateTime::Format::ISO8601->new->
-        parse_datetime(OpenSRF::Utils::cleanse_ISO8601($date));
+        parse_datetime(clean_ISO8601($date));
 
     # actor.usr.dob stores dates without time/timezone, which causes
     # DateTime to assume the date is stored as UTC.  Tell DateTime
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/SIP/Item.pm b/Open-ILS/src/perlmods/lib/OpenILS/SIP/Item.pm
index 0c598257b8..e727732e6f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/SIP/Item.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/SIP/Item.pm
@@ -9,7 +9,7 @@ use OpenILS::SIP::Transaction;
 use OpenILS::Application::AppUtils;
 # use Data::Dumper;
 use OpenILS::Const qw/:const/;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use DateTime::Format::ISO8601;
 use OpenSRF::Utils::SettingsClient;
 my $U = 'OpenILS::Application::AppUtils';
@@ -404,7 +404,7 @@ sub hold_pickup_date {
         $shelf_expire_setting_cache{$hold->pickup_lib->id} = $interval;
 
         if($interval) {
-            my $seconds = OpenSRF::Utils->interval_to_seconds($interval);
+            my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($interval);
             $date = DateTime->now->add(seconds => $seconds);
             $date = $date->strftime('%FT%T%z') if $date;
         }
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm b/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
index 9e593e5d50..3b458cb784 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
@@ -18,7 +18,7 @@ use OpenILS::SIP;
 use OpenILS::Application::AppUtils;
 use OpenILS::Application::Actor;
 use OpenILS::Const qw/:const/;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use DateTime::Format::ISO8601;
 my $U = 'OpenILS::Application::AppUtils';
 
@@ -337,7 +337,7 @@ sub charge_ok {
     my $circ_is_blocked = 0;
 
     # compute expiration date for borrowing privileges
-    my $expire = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($u->expire_date));
+    my $expire = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($u->expire_date));
 
     $circ_is_blocked =
         (($u->barred eq 't') or
@@ -356,7 +356,7 @@ sub renew_ok {
     my $renew_is_blocked = 0;
 
     # compute expiration date for borrowing privileges
-    my $expire = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($u->expire_date));
+    my $expire = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($u->expire_date));
 
     $renew_is_blocked =
         (($u->barred eq 't') or
@@ -382,7 +382,7 @@ sub hold_ok {
     my $hold_is_blocked = 0;
 
     # compute expiration date for borrowing privileges
-    my $expire = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($u->expire_date));
+    my $expire = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($u->expire_date));
 
     $hold_is_blocked =
         (($u->barred eq 't') or
@@ -450,7 +450,7 @@ sub screen_msg {
     return $b if $u->standing_penalties and @{$u->standing_penalties};
 
     # has the patron account expired?
-    my $expire = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($u->expire_date));
+    my $expire = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($u->expire_date));
     return $b if CORE::time > $expire->epoch;
 
     return '';
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/DateTime.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/DateTime.pm
new file mode 100644
index 0000000000..d6224800b4
--- /dev/null
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/DateTime.pm
@@ -0,0 +1,246 @@
+package OpenILS::Utils::DateTime;
+
+use Time::Local;
+use Errno;
+use POSIX;
+use FileHandle;
+use Digest::MD5 qw(md5 md5_hex md5_base64);
+use Exporter;
+use DateTime;
+use DateTime::Format::ISO8601;
+use DateTime::TimeZone;
+
+=head1 NAME
+
+OpenILS::Utils::DateTime;
+
+=head1 DESCRIPTION
+
+This contains several routines for doing date and time calculation. This
+is derived from the date/time routines from OpenSRF::Utils.
+
+=head1 VERSION
+
+=cut
+
+our $VERSION = 1.000;
+
+use vars qw/@ISA $AUTOLOAD %EXPORT_TAGS @EXPORT_OK @EXPORT/;
+push @ISA, 'Exporter';
+
+%EXPORT_TAGS = (
+	datetime	=> [qw(clean_ISO8601 gmtime_ISO8601 interval_to_seconds seconds_to_interval)],
+);
+Exporter::export_ok_tags('datetime');  # add aa, cc and dd to @EXPORT_OK
+
+our $date_parser = DateTime::Format::ISO8601->new;
+
+=head1 METHODS
+
+
+=cut
+
+sub AUTOLOAD {
+	my $self = shift;
+	my $type = ref($self) or return undef;
+
+	my $name = $AUTOLOAD;
+	$name =~ s/.*://;   # strip fully-qualified portion
+
+	if (defined($_[0])) {
+		return $self->{$name} = shift;
+	}
+	return $self->{$name};
+}
+
+=head2 $thing->interval_to_seconds('interval') OR interval_to_seconds('interval')
+
+=head2 $thing->seconds_to_interval($seconds) OR seconds_to_interval($seconds)
+
+Returns the number of seconds for any interval passed, or the interval for the seconds.
+This is the generic version of B<interval> listed below.
+
+The interval must match the regex I</\s*\+?\s*(\d+)\s*(\w{1})\w*\s*/g>, for example
+B<2 weeks, 3 d and 1hour + 17 Months> or
+B<1 year, 5 Months, 2 weeks, 3 days and 1 hour of seconds> meaning 46148400 seconds.
+
+	my $expire_time = time() + $thing->interval_to_seconds('17h 9m');
+
+The time size indicator may be one of
+
+=over 2
+
+=item s[econd[s]]
+
+for seconds
+
+=item m[inute[s]]
+
+for minutes
+
+=item h[our[s]]
+
+for hours
+
+=item d[ay[s]]
+
+for days
+
+=item w[eek[s]]
+
+for weeks
+
+=item M[onth[s]]
+
+for months (really (365 * 1d)/12 ... that may get smarter, though)
+
+=item y[ear[s]]
+
+for years (this is 365 * 1d)
+
+=back
+
+=cut
+sub interval_to_seconds {
+	my $self = shift;
+        my $interval = shift || $self;
+
+	$interval =~ s/(\d{2}):(\d{2}):(\d{2})/ $1 h $2 min $3 s /go;
+
+        $interval =~ s/and/,/g;
+        $interval =~ s/,/ /g;
+
+        my $amount = 0;
+        while ($interval =~ /\s*([\+-]?)\s*(\d+)\s*(\w+)\s*/g) {
+		my ($sign, $count, $type) = ($1, $2, $3);
+		$count = "$sign$count" if ($sign);
+                $amount += $count if ($type =~ /^s/);
+                $amount += 60 * $count if ($type =~ /^m(?!o)/oi);
+                $amount += 60 * 60 * $count if ($type =~ /^h/);
+                $amount += 60 * 60 * 24 * $count if ($type =~ /^d/oi);
+                $amount += 60 * 60 * 24 * 7 * $count if ($type =~ /^w/oi);
+                $amount += ((60 * 60 * 24 * 365)/12) * $count if ($type =~ /^mo/io);
+                $amount += 60 * 60 * 24 * 365 * $count if ($type =~ /^y/oi);
+        }
+        return $amount;
+}
+
+sub seconds_to_interval {
+	my $self = shift;
+        my $interval = shift || $self;
+
+        my $limit = shift || 's';
+        $limit =~ s/^(.)/$1/o;
+
+        my ($y,$ym,$M,$Mm,$w,$wm,$d,$dm,$h,$hm,$m,$mm,$s,$string);
+        my ($year, $month, $week, $day, $hour, $minute, $second) =
+                ('year','Month','week','day', 'hour', 'minute', 'second');
+
+        if ($y = int($interval / (60 * 60 * 24 * 365))) {
+                $string = "$y $year". ($y > 1 ? 's' : '');
+                $ym = $interval % (60 * 60 * 24 * 365);
+        } else {
+                $ym = $interval;
+        }
+        return $string if ($limit eq 'y');
+
+        if ($M = int($ym / ((60 * 60 * 24 * 365)/12))) {
+                $string .= ($string ? ', ':'')."$M $month". ($M > 1 ? 's' : '');
+                $Mm = $ym % ((60 * 60 * 24 * 365)/12);
+        } else {
+                $Mm = $ym;
+        }
+        return $string if ($limit eq 'M');
+
+        if ($w = int($Mm / 604800)) {
+                $string .= ($string ? ', ':'')."$w $week". ($w > 1 ? 's' : '');
+                $wm = $Mm % 604800;
+        } else {
+                $wm = $Mm;
+        }
+        return $string if ($limit eq 'w');
+
+        if ($d = int($wm / 86400)) {
+                $string .= ($string ? ', ':'')."$d $day". ($d > 1 ? 's' : '');
+                $dm = $wm % 86400;
+        } else {
+                $dm = $wm;
+        }
+        return $string if ($limit eq 'd');
+
+        if ($h = int($dm / 3600)) {
+                $string .= ($string ? ', ' : '')."$h $hour". ($h > 1 ? 's' : '');
+                $hm = $dm % 3600;
+        } else {
+                $hm = $dm;
+        }
+        return $string if ($limit eq 'h');
+
+        if ($m = int($hm / 60)) {
+                $string .= ($string ? ', ':'')."$m $minute". ($m > 1 ? 's' : '');
+                $mm = $hm % 60;
+        } else {
+                $mm = $hm;
+        }
+        return $string if ($limit eq 'm');
+
+        if ($s = int($mm)) {
+                $string .= ($string ? ', ':'')."$s $second". ($s > 1 ? 's' : '');
+        } else {
+                $string = "0s" unless ($string);
+        }
+        return $string;
+}
+
+sub gmtime_ISO8601 {
+	my $self = shift;
+	my @date = gmtime;
+
+	my $y = $date[5] + 1900;
+	my $M = $date[4] + 1;
+	my $d = $date[3];
+	my $h = $date[2];
+	my $m = $date[1];
+	my $s = $date[0];
+
+	return sprintf('%d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d+00:00', $y, $M, $d, $h, $m, $s);
+}
+
+sub clean_ISO8601 {
+	my $self = shift;
+	my $date = shift || $self;
+	if ($date =~ /^\s*(\d{4})-?(\d{2})-?(\d{2})/o) {
+		my $new_date = "$1-$2-$3";
+
+		if ($date =~/(\d{2}):(\d{2}):(\d{2})/o) {
+			$new_date .= "T$1:$2:$3";
+
+			my $z;
+			if ($date =~ /([-+]{1})([0-9]{1,2})(?::?([0-9]{1,2}))*\s*$/o) {
+				$z = sprintf('%s%0.2d%0.2d',$1,$2,$3)
+			} else {
+				$z =  DateTime::TimeZone::offset_as_string(
+					DateTime::TimeZone
+						->new( name => 'local' )
+						->offset_for_datetime(
+							$date_parser->parse_datetime($new_date)
+						)
+				);
+			}
+
+			if (length($z) > 3 && index($z, ':') == -1) {
+				substr($z,3,0) = ':';
+				substr($z,6,0) = ':' if (length($z) > 6);
+			}
+		
+			$new_date .= $z;
+		} else {
+			$new_date .= "T00:00:00";
+		}
+
+		return $new_date;
+	}
+	return $date;
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm
index 9aab446002..392822a1c0 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm
@@ -19,7 +19,7 @@ use DateTime;
 use OpenSRF::AppSession;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenSRF::Utils::JSON;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenILS::Application::AppUtils;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 
@@ -161,7 +161,7 @@ sub init {
 
     if ($self->{soft_retarget_interval}) {
 
-        my $secs = OpenSRF::Utils->interval_to_seconds(
+        my $secs = OpenILS::Utils::DateTime->interval_to_seconds(
             $self->{soft_retarget_interval});
 
         $self->{soft_retarget_time} = 
@@ -176,7 +176,7 @@ sub init {
     # it overrides the retarget_interval.
     my $next_check_secs = 
         $self->{next_check_interval} ?
-        OpenSRF::Utils->interval_to_seconds($self->{next_check_interval}) :
+        OpenILS::Utils::DateTime->interval_to_seconds($self->{next_check_interval}) :
         $retarget_seconds;
 
     my $next_check_date = 
@@ -258,7 +258,7 @@ use strict;
 use warnings;
 use DateTime;
 use OpenSRF::AppSession;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenILS::Application::AppUtils;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
@@ -403,7 +403,7 @@ sub handle_expired_hold {
     return 1 unless $hold->expire_time;
 
     my $ex_time =
-        $dt_parser->parse_datetime(cleanse_ISO8601($hold->expire_time));
+        $dt_parser->parse_datetime(clean_ISO8601($hold->expire_time));
     return 1 unless 
         DateTime->compare($ex_time, DateTime->now(time_zone => 'local')) < 0;
 
@@ -781,7 +781,7 @@ sub inspect_previous_target {
         # soft_retarget_time and the retarget_time.
 
         my $pct = $dt_parser->parse_datetime(
-            cleanse_ISO8601($hold->prev_check_time));
+            clean_ISO8601($hold->prev_check_time));
 
         $soft_retarget =
             DateTime->compare($pct, $self->parent->{retarget_time}) > 0;
@@ -1184,7 +1184,7 @@ sub process_recalls {
     # Give the user a new due date of either a full recall threshold,
     # or the return interval, whichever is further in the future.
     my $threshold_date = DateTime::Format::ISO8601
-        ->parse_datetime(cleanse_ISO8601($circ->xact_start))
+        ->parse_datetime(clean_ISO8601($circ->xact_start))
         ->add(seconds => interval_to_seconds($threshold))
         ->iso8601();
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/Penalty.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/Penalty.pm
index 94c561c218..c91bbc8d8a 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Utils/Penalty.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/Penalty.pm
@@ -4,7 +4,7 @@ use DateTime;
 use Data::Dumper;
 use OpenSRF::EX qw(:try);
 use OpenSRF::Utils::Cache;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenILS::Application::AppUtils;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
index eeeba4a078..2bf6704742 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
@@ -6,7 +6,7 @@ use Digest::MD5 qw(md5_hex);
 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
 use OpenSRF::AppSession;
 use OpenSRF::EX qw/:try/;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::JSON;
 use OpenSRF::Utils::Logger qw/$logger/;
 use OpenILS::Application::AppUtils;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
index 3cb07997db..730586a128 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
@@ -267,7 +267,7 @@ sub init_ro_object_cache {
             $date = '000' . $date;
         }
 
-        my $cleansed_date = cleanse_ISO8601($date);
+        my $cleansed_date = clean_ISO8601($date);
 
         $date = DateTime::Format::ISO8601->new->parse_datetime($cleansed_date);
         if ($context_org) {
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/Exporter.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/Exporter.pm
index 8508ab6ede..576dffd515 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/Exporter.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/Exporter.pm
@@ -16,7 +16,7 @@ use Data::Dumper;
 use Text::CSV;
 
 use OpenSRF::EX qw(:try);
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Cache;
 use OpenSRF::System;
 use OpenSRF::AppSession;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
index a5fea7e630..b43d266069 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
@@ -13,7 +13,7 @@ use SRU::Request;
 use SRU::Response;
 
 use OpenSRF::EX qw(:try);
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Cache;
 use OpenSRF::System;
 use OpenSRF::AppSession;
@@ -1639,7 +1639,7 @@ sub create_record_feed {
         }
 
         $node->id($item_tag);
-        #$node->update_ts(cleanse_ISO8601($record->edit_date));
+        #$node->update_ts(clean_ISO8601($record->edit_date));
         $node->link(alternate => $feed->unapi . "?id=$item_tag&format=opac" => 'text/html') if ($flesh > 0);
         $node->link(slimpac => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
         $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm
index 56146cb988..92064092cc 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat/Feed.pm
@@ -229,7 +229,7 @@ sub description {};
 
 package OpenILS::WWW::SuperCat::Feed::atom;
 use base 'OpenILS::WWW::SuperCat::Feed';
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 
 sub new {
     my $class = shift;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/TemplateBatchBibUpdate.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/TemplateBatchBibUpdate.pm
index fa4fc5d2c5..33d0f042f3 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/TemplateBatchBibUpdate.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/TemplateBatchBibUpdate.pm
@@ -16,7 +16,7 @@ use Data::Dumper;
 use Text::CSV;
 
 use OpenSRF::EX qw(:try);
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::Cache;
 use OpenSRF::System;
 use OpenSRF::AppSession;
diff --git a/Open-ILS/src/perlmods/live_t/03-overdue_circ.t b/Open-ILS/src/perlmods/live_t/03-overdue_circ.t
index f803843717..8323bcfe11 100644
--- a/Open-ILS/src/perlmods/live_t/03-overdue_circ.t
+++ b/Open-ILS/src/perlmods/live_t/03-overdue_circ.t
@@ -16,7 +16,7 @@ my $script = OpenILS::Utils::TestUtils->new();
 
 use DateTime;
 use DateTime::Format::ISO8601;
-use OpenSRF::Utils qw/cleanse_ISO8601/;
+use OpenILS::Utils::DateTime qw/clean_ISO8601/;
 
 our $apputils = 'OpenILS::Application::AppUtils';
 
@@ -144,8 +144,8 @@ if (my $bill_resp = $bill_req->recv) {
     }
 }
 
-my $xact_start = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->xact_start));
-my $due_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->due_date));
+my $xact_start = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($circ->xact_start));
+my $due_date = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($circ->due_date));
 
 # Rewrite history; technically we should rewrite status_changed_item on the copy as well, but, meh...
 $circ->xact_start( $xact_start->subtract( days => 20 )->iso8601() );
diff --git a/Open-ILS/src/perlmods/live_t/04-overdue_with_closed_dates.t b/Open-ILS/src/perlmods/live_t/04-overdue_with_closed_dates.t
index 780781c201..ced78bef41 100644
--- a/Open-ILS/src/perlmods/live_t/04-overdue_with_closed_dates.t
+++ b/Open-ILS/src/perlmods/live_t/04-overdue_with_closed_dates.t
@@ -16,7 +16,7 @@ my $script = OpenILS::Utils::TestUtils->new();
 
 use DateTime;
 use DateTime::Format::ISO8601;
-use OpenSRF::Utils qw/cleanse_ISO8601/;
+use OpenILS::Utils::DateTime qw/clean_ISO8601/;
 
 our $apputils   = "OpenILS::Application::AppUtils";
 
@@ -196,8 +196,8 @@ if (my $bill_resp = $bill_req->recv) {
     }
 }
 
-my $xact_start = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->xact_start));
-my $due_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->due_date));
+my $xact_start = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($circ->xact_start));
+my $due_date = DateTime::Format::ISO8601->parse_datetime(clean_ISO8601($circ->due_date));
 
 # Rewrite history; technically we should rewrite status_changed_item on the copy as well, but, meh...
 $circ->xact_start( $xact_start->subtract( days => 20 )->iso8601() );
diff --git a/Open-ILS/src/perlmods/live_t/05-pay_bills.t b/Open-ILS/src/perlmods/live_t/05-pay_bills.t
index c93a5867c2..032b7b212b 100644
--- a/Open-ILS/src/perlmods/live_t/05-pay_bills.t
+++ b/Open-ILS/src/perlmods/live_t/05-pay_bills.t
@@ -16,7 +16,7 @@ my $script = OpenILS::Utils::TestUtils->new();
 
 use DateTime;
 use DateTime::Format::ISO8601;
-use OpenSRF::Utils qw/cleanse_ISO8601/;
+use OpenILS::Utils::DateTime qw/clean_ISO8601/;
 
 our $apputils   = "OpenILS::Application::AppUtils";
 
diff --git a/Open-ILS/src/perlmods/live_t/09-lp1198465_neg_balances.t b/Open-ILS/src/perlmods/live_t/09-lp1198465_neg_balances.t
index 4702a339f1..c72f7e254f 100644
--- a/Open-ILS/src/perlmods/live_t/09-lp1198465_neg_balances.t
+++ b/Open-ILS/src/perlmods/live_t/09-lp1198465_neg_balances.t
@@ -11,7 +11,7 @@ use strict; use warnings;
 
 use DateTime;
 use DateTime::Format::ISO8601;
-use OpenSRF::Utils qw/cleanse_ISO8601/;
+use OpenILS::Utils::DateTime qw/clean_ISO8601/;
 use OpenILS::Utils::TestUtils;
 my $script = OpenILS::Utils::TestUtils->new();
 use Data::Dumper;
diff --git a/Open-ILS/src/perlmods/t/14-OpenILS-Utils.t b/Open-ILS/src/perlmods/t/14-OpenILS-Utils.t
index 548bea3101..dc6e2a1162 100644
--- a/Open-ILS/src/perlmods/t/14-OpenILS-Utils.t
+++ b/Open-ILS/src/perlmods/t/14-OpenILS-Utils.t
@@ -1,6 +1,6 @@
 #!perl -T
 
-use Test::More tests => 30;
+use Test::More tests => 39;
 use Test::Warn;
 use utf8;
 
@@ -21,6 +21,7 @@ use_ok( 'OpenILS::Utils::RemoteAccount' );
 use_ok( 'OpenILS::Utils::ZClient' );
 use_ok( 'OpenILS::Utils::EDIReader' );
 use_ok( 'OpenILS::Utils::HTTPClient' );
+use_ok( 'OpenILS::Utils::DateTime' );
 
 # LP 800269 - Test MFHD holdings for records that only contain a caption field
 my $co_marc = MARC::Record->new();
@@ -98,3 +99,11 @@ is($edi_msgs->[0]->{purchase_order}, '24', 'edi reader: PO number');
 is($edi_msgs->[1]->{invoice_ident}, '5TST084027', 'edi reader: invoice ident');
 is(scalar(@{$edi_msgs->[1]->{lineitems}}), '2', 'edi reader: lineitem count');
 
+is (OpenILS::Utils::DateTime::interval_to_seconds('1 second'), 1);
+is (OpenILS::Utils::DateTime::interval_to_seconds('1 minute'), 60);
+is (OpenILS::Utils::DateTime::interval_to_seconds('1 hour'), 3600);
+is (OpenILS::Utils::DateTime::interval_to_seconds('1 day'), 86400);
+is (OpenILS::Utils::DateTime::interval_to_seconds('1 week'), 604800);
+is (OpenILS::Utils::DateTime::interval_to_seconds('1 month'), 2628000);
+is (OpenILS::Utils::DateTime::interval_to_seconds('1 year'), 31536000);
+is (OpenILS::Utils::DateTime::interval_to_seconds('1 year 1 second'), 31536001);
diff --git a/Open-ILS/src/support-scripts/generate_circ_notices.pl b/Open-ILS/src/support-scripts/generate_circ_notices.pl
index 998cc227ab..b27f5d8096 100755
--- a/Open-ILS/src/support-scripts/generate_circ_notices.pl
+++ b/Open-ILS/src/support-scripts/generate_circ_notices.pl
@@ -23,7 +23,7 @@ use Email::Send;
 use Getopt::Long;
 use Unicode::Normalize;
 use DateTime::Format::ISO8601;
-use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::DateTime qw/:datetime/;
 use OpenSRF::Utils::JSON;
 use OpenSRF::Utils::SettingsClient;
 use OpenSRF::AppSession;
@@ -128,12 +128,12 @@ sub main {
     $predue_notices = [$predue_notices] unless ref $predue_notices eq 'ARRAY'; 
 
     my @overdues = sort { 
-        OpenSRF::Utils->interval_to_seconds($a->{notify_interval}) <=> 
-        OpenSRF::Utils->interval_to_seconds($b->{notify_interval}) } @$overdue_notices;
+        OpenILS::Utils::DateTime->interval_to_seconds($a->{notify_interval}) <=> 
+        OpenILS::Utils::DateTime->interval_to_seconds($b->{notify_interval}) } @$overdue_notices;
 
     my @predues = sort { 
-        OpenSRF::Utils->interval_to_seconds($a->{notify_interval}) <=> 
-        OpenSRF::Utils->interval_to_seconds($b->{notify_interval}) } @$predue_notices;
+        OpenILS::Utils::DateTime->interval_to_seconds($a->{notify_interval}) <=> 
+        OpenILS::Utils::DateTime->interval_to_seconds($b->{notify_interval}) } @$predue_notices;
 
     for my $db (($opt_days_back) ? split(',', $opt_days_back) : 0) {
         if($opt_notice_types =~ /overdue/) {
@@ -174,7 +174,7 @@ sub global_overdue_output {
 sub generate_notice_set {
     my($notice, $type, $days_back) = @_;
 
-    my $notify_interval = OpenSRF::Utils->interval_to_seconds($notice->{notify_interval});
+    my $notify_interval = OpenILS::Utils::DateTime->interval_to_seconds($notice->{notify_interval});
     $notify_interval = -$notify_interval if $type eq 'overdue';
 
     my ($start_date, $end_date) = make_date_range($notify_interval - $days_back * 86400);
@@ -328,7 +328,7 @@ sub get_bib_attr {
 # provides a date that Template::Plugin::Date can parse
 sub parse_due_date {
     my $circ = shift;
-    my $due = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
+    my $due = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($circ->due_date));
     return sprintf(
         "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
         $due->hour,
diff --git a/Open-ILS/src/support-scripts/set_pbx_holidays.pl b/Open-ILS/src/support-scripts/set_pbx_holidays.pl
index 15483c0c97..aea2117b7a 100755
--- a/Open-ILS/src/support-scripts/set_pbx_holidays.pl
+++ b/Open-ILS/src/support-scripts/set_pbx_holidays.pl
@@ -4,7 +4,7 @@ require "/openils/bin/oils_header.pl";
 
 use strict;
 use warnings;
-use OpenSRF::Utils qw/cleanse_ISO8601/;
+use OpenILS::Utils::DateTime qw/clean_ISO8601/;
 use OpenSRF::Utils::Logger qw/$logger/;
 use OpenSRF::Utils::SettingsClient;
 
@@ -16,7 +16,7 @@ use Getopt::Std;
 sub unixify {
     my ($stringy_ts) = @_;
     return (new DateTime::Format::ISO8601)->parse_datetime(
-        cleanse_ISO8601($stringy_ts)
+        clean_ISO8601($stringy_ts)
     )->epoch;
 }
 
-- 
2.11.0