diff --git a/Conch/cpanfile b/Conch/cpanfile index 49b313f80..b28adcfaf 100644 --- a/Conch/cpanfile +++ b/Conch/cpanfile @@ -12,7 +12,6 @@ requires 'aliased'; requires 'Try::Tiny'; requires 'Class::StrongSingleton'; requires 'Time::HiRes'; -requires 'Time::Moment'; # mojolicious requires 'Mojolicious'; @@ -72,5 +71,4 @@ on 'test' => sub { requires 'IO::All'; requires 'JSON::Validator'; requires 'YAML::XS'; - requires 'Test::Exception'; }; diff --git a/Conch/cpanfile.snapshot b/Conch/cpanfile.snapshot index 2306d71ec..f2fc74e9e 100644 --- a/Conch/cpanfile.snapshot +++ b/Conch/cpanfile.snapshot @@ -285,7 +285,6 @@ DISTRIBUTIONS Config::Any::XML undef Config::Any::YAML undef requirements: - Config::General 2.47 Module::Pluggable::Object 3.6 Config-General-2.63 pathname: T/TL/TLINDEN/Config-General-2.63.tar.gz @@ -2147,6 +2146,27 @@ DISTRIBUTIONS Fennec::Lite 0 Test::Exception 0 Test::More 0 + Minion-8.11 + pathname: S/SR/SRI/Minion-8.11.tar.gz + provides: + LinkCheck undef + LinkCheck::Controller::Links undef + LinkCheck::Task::CheckLinks undef + Minion 8.11 + Minion::Backend undef + Minion::Backend::Pg undef + Minion::Command::minion undef + Minion::Command::minion::job undef + Minion::Command::minion::worker undef + Minion::Job undef + Minion::Worker undef + Minion::_Guard 8.11 + Mojolicious::Plugin::Minion undef + Mojolicious::Plugin::Minion::Admin undef + requirements: + ExtUtils::MakeMaker 0 + Mojolicious 7.56 + perl 5.010001 Mock-Quick-1.111 pathname: E/EX/EXODIST/Mock-Quick-1.111.tar.gz provides: @@ -2326,8 +2346,8 @@ DISTRIBUTIONS Mojolicious 7.53 SQL::Abstract 1.85 perl 5.010001 - Mojolicious-7.61 - pathname: S/SR/SRI/Mojolicious-7.61.tar.gz + Mojolicious-7.70 + pathname: S/SR/SRI/Mojolicious-7.70.tar.gz provides: Mojo undef Mojo::Asset undef @@ -2396,7 +2416,7 @@ DISTRIBUTIONS Mojo::UserAgent::Transactor undef Mojo::Util undef Mojo::WebSocket undef - Mojolicious 7.61 + Mojolicious 7.70 Mojolicious::Command undef Mojolicious::Command::cgi undef Mojolicious::Command::cpanify undef @@ -3910,28 +3930,6 @@ DISTRIBUTIONS Exporter::Tiny 0.026 ExtUtils::MakeMaker 6.17 perl 5.006001 - Types-UUID-0.004 - pathname: T/TO/TOBYINK/Types-UUID-0.004.tar.gz - provides: - Types::UUID 0.004 - requirements: - ExtUtils::MakeMaker 6.17 - Type::Tiny 1.000000 - UUID::Tiny 1.02 - perl 5.008 - UUID-Tiny-1.04 - pathname: C/CA/CAUGUSTIN/UUID-Tiny-1.04.tar.gz - provides: - UUID::Tiny 1.04 - requirements: - Carp 0 - Digest::MD5 0 - ExtUtils::MakeMaker 0 - IO::File 0 - MIME::Base64 0 - POSIX 0 - Test::More 0 - Time::HiRes 0 Unicode-LineBreak-2017.004 pathname: N/NE/NEZUMI/Unicode-LineBreak-2017.004.tar.gz provides: @@ -4008,6 +4006,20 @@ DISTRIBUTIONS requirements: ExtUtils::MakeMaker 0 perl 5.008001 + YAML-Tiny-1.70 + pathname: E/ET/ETHER/YAML-Tiny-1.70.tar.gz + provides: + YAML::Tiny 1.70 + requirements: + B 0 + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 0 + Fcntl 0 + Scalar::Util 0 + perl 5.008001 + strict 0 + warnings 0 aliased-0.34 pathname: E/ET/ETHER/aliased-0.34.tar.gz provides: diff --git a/Conch/lib/Conch/Time.pm b/Conch/lib/Conch/Time.pm index 0680bdc73..c8e914f12 100644 --- a/Conch/lib/Conch/Time.pm +++ b/Conch/lib/Conch/Time.pm @@ -11,6 +11,7 @@ Conch::Time - format Postgres Timestamps as RFC 3337 UTC timestamps my $postgres_timestamp = '2018-01-26 12:24:18.893874-07'; my $time = Conch::Time->new($postgres_timestamp); + say $time; # '2018-01-26T12:24:18.893Z' $time eq $time; # 1 @@ -22,61 +23,46 @@ package Conch::Time; use Mojo::Base -base, -signatures; use POSIX qw(strftime); -use Time::Moment; use Time::HiRes; +use DateTime::Format::Strptime; use Mojo::Exception; use overload - '""' => 'rfc3339', + '""' => 'to_string', eq => 'compare', ne => sub { !compare(@_) }; use constant PG_TIMESTAMP_FORMAT => qr/ ^(\d{4,})-(\d{2,})-(\d{2,})\s - (\d{2,}):(\d{2,}):(\d{2,})\.?(\d+) - ?([-\+])([\d:]+)$ + (\d{2,}):(\d{2,}):(\d{2,})(\.\d+) + ?([-\+][\d:]+)$ /x; +=head2 timestamp +Underlying RFC 3339 formatted timestamp -has 'moment'; - -=head2 new +=cut - Conch::Time->new($pg_timestamptz); +has 'timestamp'; +=head2 new =cut + sub new ( $class, $timestamptz ) { - my @c = ( $timestamptz =~ PG_TIMESTAMP_FORMAT ); Mojo::Exception->throw('Invalid Postgres timestamp') - unless @c; - - $c[6] = 0 unless $c[6]; - - my $off_minutes; - if ($c[8] =~ /:/) { - my ($hours, $minutes) = $c[8] =~ /^(\d\d)[:]?(\d\d)$/; - - $off_minutes = ($hours*60); - $off_minutes = $off_minutes + $minutes if $minutes; - } else { - $off_minutes = $c[8] * 60; - } - - my $m = Time::Moment->new( - year => $c[0], - month => $c[1], - day => $c[2], - hour => $c[3], - minute => $c[4], - second => $c[5], - nanosecond => $c[6]*1000, - offset => "${c[7]}${off_minutes}", - ); - return $class->SUPER::new(moment => $m); + unless $timestamptz && ( $timestamptz =~ m/${\PG_TIMESTAMP_FORMAT}/ ); + my $dt = "$1-$2-$3T$4:$5:$6." . _normalize_millisec($7) . _normalize_tz($8); + $class->SUPER::new( timestamp => $dt ); } +sub _from_hires($class, $epoch, $mil) { + my $dt = strftime("%Y-%m-%dT%H:%M:%S", gmtime($epoch)) . + _normalize_millisec($mil) . "Z"; + + return $class->SUPER::new(timestamp => $dt); +} =head2 now @@ -86,87 +72,67 @@ sub new ( $class, $timestamptz ) { Return an object based on the current time. Time are high resolution and will generate unique timestamps to the -nanosecond. +millisecond. =cut sub now ($class) { - return $class->SUPER::new(moment => Time::Moment->now()); + return $class->_from_hires(Time::HiRes::gettimeofday()); } -=head2 from_epoch - - Conch::Time->from_epoch(time()); - - Conch::Time->from_epoch(Time::HiRes::gettimeofday); - -=cut - -sub from_epoch ($class, $epoch, $nano = 0) { - return $class->SUPER::new(moment => Time::Moment->from_epoch( - $epoch, - $nano, - )); +# Given a float, return the number of integer milliseconds it represents +sub _normalize_millisec { + substr( sprintf( '%.3f', shift || 0 ), 2 ); } +sub _normalize_tz { + my $tz = shift; + # return 'Z' if the timezone is 00 or 00:00 + return 'Z' if $tz =~ /^[-\+]00(?!:[1-9]\d)/; + # Append :00 if the timezone doesn't specify minutes + return $tz . ':00' if $tz =~ /^[-\+]\d\d$/; -=head2 compare - -Compare two Conch::Time objects. Used to overload C and C. + # Munge offsets like -0500 into -05:00 + return "$1$2:$3" if $tz =~ /^([-\+])(\d\d)(\d\d)$/; -=cut - -sub compare { - my ( $self, $other ) = @_; - return $self->moment->is_equal($other->moment) + return $tz; } +=head2 to_datetime -=head2 CONVERSIONS +Return a C object representing the timestamp. -=head3 rfc3339 - -Return an RFC3339 compatible string +B This method will negatively impact performance if called frequently. =cut -sub rfc3339 { - my $self = shift; - return $self->moment->strftime("%Y-%m-%dT%H:%M:%S.%3N%Z"); +sub to_datetime { + return DateTime::Format::Strptime->new( + pattern => '%Y-%m-%dT%H:%M:%S.%3N%z', + on_error => 'croak' + )->parse_datetime( shift->timestamp ); } +=head2 compare - -=head3 timestamp - -Return an RFC3339 compatible string +Compare two Conch::Time objects. Used to overload C and C. =cut -sub timestamp { shift->rfc3339() } - - +sub compare { + my ( $self, $other ) = @_; + $self->timestamp eq $other->timestamp; +} -=head3 to_string +=head2 to_string -Render the timestamp as a RFC 3339 timestamp string. Used to +Render the timestamp as a RFC 3337 timestamp string. Used to overload string coercion. =cut -sub to_string { shift->rfc3339 } - - - - -=head3 timestamptz - -Render a string in PostgreSQL's timestamptz style - -=cut - -sub timestamptz { - return shift->moment->strftime("%Y-%m-%d %H:%M:%S%f%z"); +sub to_string { + shift->timestamp; } 1; diff --git a/Conch/t/conch_time.t b/Conch/t/conch_time.t index 8eb8a77b3..38d5e8f5d 100644 --- a/Conch/t/conch_time.t +++ b/Conch/t/conch_time.t @@ -1,9 +1,9 @@ use Mojo::Base -strict; use Test::More; use Test::ConchTmpDB; -use Test::Exception; use Mojo::Pg; -use Time::HiRes; + +use DDP; use_ok("Conch::Time"); use Conch::Time; @@ -15,6 +15,7 @@ my $pg = Mojo::Pg->new( $pgtmp->uri ); subtest 'Test timestamps from real DB' => sub { my $now = $pg->db->query('SELECT NOW()::timestamptz as now ')->hash->{now}; ok( my $conch_time = Conch::Time->new($now) ); + isa_ok( $conch_time->to_datetime, 'DateTime', 'Can produce DateTime object' ); my $dt = $pg->db->query("SELECT '2018-01-02'::timestamptz as datetime") ->hash->{datetime}; @@ -79,6 +80,11 @@ subtest 'Test parsing of timestamps' => sub { expected => '2018-01-02T00:00:00.000+00:20', message => 'Does not modify 00 timezones that specify minutes' }, + { + input => '2018-01-02 00:00:00.987654+00', + expected => '2018-01-02T00:00:00.988Z', + message => 'Microseconds rounded to milliseconds' + }, ); for (@cases) { @@ -87,23 +93,8 @@ subtest 'Test parsing of timestamps' => sub { } }; -my $d; -lives_ok { - $d = Conch::Time->from_epoch(1519922279, 0); -} "->from_epoch with static input"; - -is($d->timestamp, "2018-03-01T16:37:59.000Z", "->_from_epoch output"); - -lives_ok { - $d = Conch::Time->from_epoch(Time::HiRes::gettimeofday); -} "->from_epoch with gettimeofday"; - +my $d = Conch::Time->_from_hires(1519922279, 0); +is($d->timestamp, "2018-03-01T16:37:59000Z", "->_from_hires output"); isnt(Conch::Time->now(), Conch::Time->now(), "Multiple now()s are unique"); -like( - Conch::Time->new("2018-01-02 00:00:00+00")->timestamptz, - Conch::Time->PG_TIMESTAMP_FORMAT, - "Roundtrip timestamptz" -); - done_testing();