Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

##----------------------------------------------------------------------------
## Module Generic - ~/lib/Module/Generic/DateTime.pm
## Version v0.5.0
## Copyright(c) 2022 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2021/03/20
## Modified 2023/09/05
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
BEGIN
{
use strict;
use warnings;
use vars qw( $ERROR $TS_RE $VERSION $HAS_LOCAL_TZ );
use DateTime 1.57;
# use Nice::Try dont_want => 1;
use Scalar::Util ();
use overload (
q{""} => sub{ $_[0]->{dt}->stringify },
bool => sub{1},
q{>} => sub{ &op( @_, '>' ) },
q{>=} => sub{ &op( @_, '>=' ) },
q{<} => sub{ &op( @_, '<' ) },
q{<=} => sub{ &op( @_, '<=' ) },
q{==} => sub{ &op( @_, '==' ) },
q{!=} => sub{ &op( @_, '!=' ) },
q{-} => sub{ &op_minus_plus( @_, '-' ) },
q{+} => sub{ &op_minus_plus( @_, '+' ) },
fallback => 1,
);
$TS_RE = qr/
(?<year>\d{4})
-
(?<month>\d{1,2})
-
(?<day>\d{1,2})
(?:
([[:blank:]]+|T)
(?<hour>\d{1,2})
\:
(?<minute>\d{1,2})
\:
(?<second>\d{1,2})
(?<tz_offset>
(?:
(?<tz_sign>[-+])
(?<tz_offset1>\d{1,2})(?<tz_offset2>\d{2})
)
)?
)?
/x;
our $VERSION = 'v0.5.0';
};
BEGIN
{
unless( defined( &DateTime::FREEZE ) )
{
*DateTime::FREEZE = sub
{
my $self = shift( @_ );
my $class = ref( $self );
my $params = {};
for( qw( utc_rd_days utc_rd_secs rd_nanosecs locale tz formatter ) )
{
$params->{ $_ } = $self->{ $_ };
}
# not used yet, but may be handy in the future.
$params->{version} = ( $DateTime::VERSION || 'git' );
return( [$class, $params] );
};
}
unless( defined( &DateTime::THAW ) )
{
*DateTime::THAW = sub
{
my( $this, $serialiser, $ref ) = @_;
my( $class, $params ) = @$ref;
my( $locale, $tz, $formatter ) = @$params{qw( locale tz formatter )};
delete( $params->{version} );
if( ref( $locale ) eq 'ARRAY' )
{
$locale = $locale->[0] if( ref( $locale->[0] ) eq 'ARRAY' );
my $locale_class = $locale->[0];
$locale = &{"${locale_class}\::THAW"}( $locale_class, $serialiser, $locale );
}
if( ref( $tz ) eq 'ARRAY' )
{
$tz = $tz->[0] if( ref( $tz->[0] ) eq 'ARRAY' );
my $tz_class = $tz->[0];
$tz = &{"${tz_class}\::THAW"}( $tz_class, $serialiser, $tz );
}
my $object = bless({
utc_vals => [ @$params{qw(utc_rd_days utc_rd_secs rd_nanosecs)} ],
tz => $tz,
}, 'DateTime::_Thawed' );
my %formatter = defined( $params->{formatter} ) ? ( formatter => $params->{formatter} ) : ();
my $new = $class->from_object(
object => $object,
locale => $locale,
%formatter,
);
return( $new );
};
}
unless( defined( &DateTime::TimeZone::FREEZE ) )
{
*DateTime::TimeZone::FREEZE = sub
{
my $self = shift;
my $class = ref( $self ) || $self;
return( [ $class, $self->name ] );
};
}
unless( defined( &DateTime::TimeZone::THAW ) )
{
*DateTime::TimeZone::THAW = sub
{
my( $this, $serialiser, $serial ) = @_;
my( $class, $tzone ) = @$serial;
my $self = $class->new( name => $tzone );
return( $self );
};
}
unless( defined( &DateTime::TimeZone::OffsetOnly::FREEZE ) )
{
*DateTime::TimeZone::OffsetOnly::FREEZE = sub
{
my( $self, undef ) = @_;
my $class = ref( $self );
return( [$class, $self->name] );
};
}
unless( defined( &DateTime::TimeZone::OffsetOnly::THAW ) )
{
*DateTime::TimeZone::OffsetOnly::THAW = sub
{
my( $this, $serialiser, $serial ) = @_;
my( $class, $name ) = @$serial;
my $self = $class->new( offset => $name );
return( $self );
};
}
unless( defined( &DateTime::Locale::FromData::FREEZE ) )
{
*DateTime::Locale::FromData::FREEZE = sub
{
my( $self, undef ) = @_;
my $class = ref( $self );
return( [$class, $self->code] );
};
}
unless( defined( &DateTime::Locale::FromData::THAW ) )
{
*DateTime::Locale::FromData::THAW = sub
{
my( $self, undef, $ref ) = @_;
my( $class, $code ) = @$ref;
my $new = DateTime::Locale->load( $code );
return( $new );
};
}
unless( defined( &DateTime::Locale::Base::FREEZE ) )
{
*DateTime::Locale::Base::FREEZE = sub
{
my( $self, undef ) = @_;
my $class = ref( $self );
return( [$class, $self->id] );
};
}
unless( defined( &DateTime::Locale::Base::THAW ) )
{
*DateTime::Locale::Base::THAW = sub
{
my( $self, undef, $ref ) = @_;
my( $class, $id ) = @$ref;
my $new = DateTime::Locale->load( $id );
return( $new );
};
}
Module::Generic->_implement_freeze_thaw( qw( DateTime::TimeZone::UTC ) );
};
# use strict;
no warnings 'redefine';
sub new
{
my $this = shift( @_ );
my $dt;
# Module::Generic::DateTime->new( $datetime_object );
# Module::Generic::DateTime->new( $datetime_object, $hash_ref_of_options );
# Module::Generic::DateTime->new( $datetime_object, %hash_of_options );
# Module::Generic::DateTime->new( $datetime_object, %hash_of_options );
# Module::Generic::DateTime->new( $hash_ref_of_options );
# Module::Generic::DateTime->new( %hash_of_options );
if( (
( @_ % 2 ) &&
(
( scalar( @_ ) == 1 && ref( $_[0] ) ne 'HASH' ) ||
scalar( @_ ) > 1
)
) ||
( scalar( @_ ) == 2 && ref( $_[1] ) eq 'HASH' ) )
{
$dt = shift( @_ );
}
my $opts = $this->_get_args_as_hash( @_ );
if( defined( $dt ) && length( $dt ) )
{
if( Scalar::Util::blessed( $dt ) )
{
if( !$dt->isa( 'DateTime' ) )
{
return( $this->error( "Object provided is not a DateTime object." ) );
}
}
else
{
return( $this->error( "First argument provided, among the odd number of parameters received, is not a DateTime object." ) );
}
}
else
{
# try-catch
local $@;
eval
{
if( !exists( $opts->{formatter} ) )
{
$opts->{formatter} = DateTime::Format::Strptime->new(
pattern => "%FT%T%z",
locale => "en_GB",
);
}
$dt = DateTime->now( %$opts );
};
if( $@ )
{
if( $@ =~ /Cannot[[:blank:]\h]+determine[[:blank:]\h]+local[[:blank:]\h]+time[[:blank:]\h]+zone/i )
{
warn( "Warning: Your system is missing key timezone components. Module::Generic::DateTime is reverting to UTC instead of local time zone." );
$opts->{time_zone} = 'UTC';
$dt = DateTime->new( %$opts );
my $dt_fmt = DateTime::Format::Strptime->new(
pattern => '%FT%T%z',
locale => 'en_GB',
);
$dt->set_formatter( $dt_fmt );
}
else
{
return( $this->error( "Error while creating a DateTime object: $@" ) );
}
}
}
return( bless( { dt => $dt->clone } => ( ref( $this ) || $this ) )->init( @_ ) );
}
# This class does not convert to an HASH, but the TO_JSON method will convert to a string
sub as_hash { return( $_[0] ); }
sub as_string { return( shift->stringify( @_ ) ); }
sub datetime { return( shift->_set_get_object_without_init( 'dt' => 'DateTime' ) ); }
sub from_epoch
{
my $this = shift( @_ );
my $dt;
# try-catch
local $@;
eval
{
$dt = DateTime->from_epoch( @_ );
};
if( $@ )
{
return( $this->error( "Error trying to create a new DateTime object using new_from_epoch(): $@" ) );
}
return( $this->new( $dt ) );
}
sub now
{
my $this = shift( @_ );
my $dt;
# try-catch
local $@;
eval
{
$dt = DateTime->now( @_ );
};
if( $@ )
{
return( $this->error( "Error trying to create a new DateTime object: $@" ) );
}
return( $this->new( $dt ) );
}
sub op
{
no overloading;
my( $self, $other, $swap, $op ) = @_;
my $class = ref( $self ) || $self;
no strict;
my $dt1 = $self->{dt};
my $dt2;
if( Scalar::Util::blessed( $other ) && $other->isa( 'DateTime' ) )
{
$dt2 = $other;
}
elsif( Scalar::Util::blessed( $other ) && ref( $other ) eq ref( $self ) )
{
$dt2 = $other->{dt};
}
# Might trigger an error if this does not work with DateTime, but that's the developer's problem
elsif( Scalar::Util::blessed( $other ) )
{
$dt2 = $other;
}
# Unix time
elsif( $other =~ /^\d{10}$/ )
{
if( !defined( $HAS_LOCAL_TZ ) )
{
# try-catch
local $@;
eval
{
$dt2 = DateTime->from_epoch( epoch => $other, time_zone => 'local' );
$HAS_LOCAL_TZ = 1;
};
if( $@ )
{
warn( "Your system is missing key timezone components. ${class} is reverting to UTC instead of local time zone.\n" );
$dt2 = DateTime->from_epoch( epoch => $other, time_zone => 'UTC' );
$HAS_LOCAL_TZ = 0;
}
}
else
{
# try-catch
local $@;
eval
{
$dt2 = DateTime->from_epoch( epoch => $other, time_zone => ( $HAS_LOCAL_TZ ? 'local' : 'UTC' ) );
};
if( $@ )
{
warn( "Error trying to set a DateTime object using ", ( $HAS_LOCAL_TZ ? 'local' : 'UTC' ), " time zone\n" );
$dt2 = DateTime->from_epoch( epoch => $other, time_zone => 'UTC' );
}
}
$dt2->set_formatter( $self->formatter );
}
elsif( $other =~ /^$TS_RE$/ )
{
my $hash = {};
my $re = { %+ };
my $offset;
@$hash{ qw( year month day hour minute second ) } = @$re{ qw( year month day hour minute second ) };
for( keys( %$hash ) )
{
$hash->{ $_ } = int( $hash->{ $_ } );
}
if( $re->{tz_offset1} )
{
$offset = 3600 * $re->{tz_offset1};
$offset += 60 * $re->{tz_offset2} if( length( $re->{tz_offset2} ) );
$offset *= -1 if( $re->{tz_sign} && $re->{tz_sign} ne '-' );
$re->{tz_offset} = $re->{tz_sign} . $re->{tz_offset1} . $re->{tz_offset2};
}
# try-catch
local $@;
eval
{
$dt2 = DateTime->new( %$hash );
$dt2->set_time_zone( $re->{tz_offset} ) if( length( $re->{tz_offset} ) );
my $dt3 = $dt2->clone;
$dt3->set_time_zone( 'UTC' );
};
if( $@ )
{
warn( "Unable to create DateTime object from parsing '$other': $@\n" );
}
}
my $eval = $swap ? "\$dt2 $op \$dt1" : "\$dt1 $op \$dt2";
# I do not want to localise $@ so it can be checked by the caller
my $res = eval( $eval );
return( $res );
}
sub op_minus_plus
{
no overloading;
my( $self, $other, $swap, $op ) = @_;
my $class = ref( $self ) || $self;
my $dt1 = $self->{dt};
$other = $self->_get_other( $other );
if( Scalar::Util::blessed( $other ) && $other->isa( 'DateTime::Duration' ) )
{
## Duration [+-] DateTime => update the datetime object in place
if( $swap )
{
if( $op eq '-' )
{
$dt1->subtract_duration( $other );
}
else
{
$dt1->add_duration( $other );
}
return( $self );
}
else
{
my $clone = !defined( $swap ) ? $dt1 : $dt1->clone;
if( $op eq '-' )
{
$clone->subtract_duration( $other );
}
else
{
$clone->add_duration( $other );
}
return( !defined( $swap ) ? $self : $self->_make_my_own( $clone ) );
}
}
elsif( Scalar::Util::blessed( $other ) && $other->isa( 'DateTime' ) )
{
if( $op eq '-' )
{
# return( $swap ? $other->subtract( $dt1 ) : $dt1->subtract( $other ) );
return( $self->_make_my_own( $swap ? ( $other - $dt1 ) : ( $dt1 - $other ) ) );
}
else
{
return( $self->_make_my_own( $swap ? ( $other + $dt1 ) : ( $dt1 + $other ) ) );
}
}
my $v;
$v = "$other" if( !ref( $other ) || ( ref( $other ) && overload::Method( $other => '""' ) ) );
die( "\$other (", overload::StrVal( $other // '' ), ") is not a number, a DateTime, or a DateTime::Duration object!\n" ) if( !defined( $v ) || $v !~ /^(?:$RE{num}{real}|$RE{num}{int})$/ );
my $new_dt;
if( $op eq '-' )
{
if( $swap )
{
# try-catch
local $@;
my( $clone, $ts );
eval
{
$clone = $dt1->clone;
$ts = $clone->epoch;
};
if( $@ )
{
die( "Error cloning and getting epoch value for DateTime object: $@" );
}
if( !defined( $HAS_LOCAL_TZ ) )
{
# try-catch
local $@;
eval
{
$clone->set_time_zone( 'local' );
$HAS_LOCAL_TZ = 1;
};
if( $@ )
{
$clone->set_time_zone( 'UTC' );
$HAS_LOCAL_TZ = 0;
warn( "Your system is missing key timezone components. ${class} is reverting to UTC instead of local time zone.\n" ) if( warnings::enabled() );
}
}
else
{
# try-catch
local $@;
eval
{
$clone->set_time_zone( $HAS_LOCAL_TZ ? 'local' : 'UTC' );
};
if( $@ )
{
warn( "Error trying to set the DateTime object time zone using ", ( $HAS_LOCAL_TZ ? 'local' : 'UTC' ), "\n" );
$clone->set_time_zone( 'UTC' );
}
}
# try-catch
local $@;
my $new_ts = $v - $ts;
eval
{
$new_dt = DateTime->from_epoch( epoch => $new_ts, time_zone => $dt1->time_zone );
my $strp = DateTime::Format::Strptime->new(
pattern => '%s',
locale => 'en_GB',
time_zone => $new_dt->time_zone,
);
$new_dt->set_formatter( $strp );
};
if( $@ )
{
die( "Error instantiating a new DateTime object with epoch timestamp $new_ts and time zone ", $dt1->time_zone );
}
}
else
{
# try-catch
local $@;
eval
{
my $clone = !defined( $swap ) ? $dt1 : $dt1->clone;
$new_dt = $clone->subtract( seconds => $v );
};
if( $@ )
{
die( "Failed to subtract ", ( $swap ? $self : $v ), " from ", ( $swap ? $v : $self ), ": $@" );
}
# If $swap is undefined, this is an assignment operation such as -=
return( $self ) if( !defined( $swap ) );
}
}
# +
else
{
if( $swap )
{
$new_dt = $dt1->add( seconds => $v );
}
else
{
# try-catch
local $@;
eval
{
my $clone = !defined( $swap ) ? $dt1 : $dt1->clone;
$new_dt = $clone->add( seconds => $v );
};
if( $@ )
{
die( "Failed to add ", ( $swap ? $self : $v ), " to ", ( $swap ? $v : $self ), ": $@" );
}
return( $self ) if( !defined( $swap ) );
}
}
return( $self->_make_my_own( $new_dt ) );
}
sub _get_other
{
my( $self, $other ) = @_;
if( Scalar::Util::blessed( $other ) )
{
if( $other->isa( 'Module::Generic::DateTime' ) )
{
$other = $other->{dt};
}
elsif( $other->isa( 'Module::Generic::DateTime::Interval' ) )
{
$other = $other->{interval};
}
}
return( $other );
}
sub _make_my_own
{
my( $self, $res ) = @_;
if( Scalar::Util::blessed( $res ) &&
$res->isa( 'DateTime::Duration' ) )
{
return( Module::Generic::DateTime::Interval->new( $res ) );
}
elsif( Scalar::Util::blessed( $res ) &&
$res->isa( 'DateTime' ) )
{
return( $self->new( $res ) );
}
else
{
return( $res );
}
}
sub FREEZE
{
my $self = CORE::shift( @_ );
my $serialiser = CORE::shift( @_ ) // '';
my $class = CORE::ref( $self );
my %hash = %$self;
# Return an array reference rather than a list so this works with Sereal and CBOR
# On or before Sereal version 4.023, Sereal did not support multiple values returned
CORE::return( [$class, \%hash] ) if( $serialiser eq 'Sereal' && Sereal::Encoder->VERSION <= version->parse( '4.023' ) );
# But Storable want a list with the first element being the serialised element
CORE::return( $class, \%hash );
}
sub STORABLE_freeze { CORE::return( CORE::shift->FREEZE( @_ ) ); }
sub STORABLE_thaw { CORE::return( CORE::shift->THAW( @_ ) ); }
# NOTE: CBOR will call the THAW method with the stored classname as first argument, the constant string CBOR as second argument, and all values returned by FREEZE as remaining arguments.
# NOTE: Storable calls it with a blessed object it created followed with $cloning and any other arguments initially provided by STORABLE_freeze
sub THAW
{
my( $self, undef, @args ) = @_;
my $ref = ( CORE::scalar( @args ) == 1 && CORE::ref( $args[0] ) eq 'ARRAY' ) ? CORE::shift( @args ) : \@args;
my $class = ( CORE::defined( $ref ) && CORE::ref( $ref ) eq 'ARRAY' && CORE::scalar( @$ref ) > 1 ) ? CORE::shift( @$ref ) : ( CORE::ref( $self ) || $self );
my $hash = CORE::ref( $ref ) eq 'ARRAY' ? CORE::shift( @$ref ) : {};
my $new;
# Storable pattern requires to modify the object it created rather than returning a new one
if( CORE::ref( $self ) )
{
foreach( CORE::keys( %$hash ) )
{
$self->{ $_ } = CORE::delete( $hash->{ $_ } );
}
$new = $self;
}
else
{
$new = CORE::bless( $hash => $class );
}
CORE::return( $new );
}
sub TO_JSON
{
my $self = CORE::shift( @_ );
CORE::return( '' ) if( !$self->{dt} || !Scalar::Util::blessed( $self->{dt} ) );
CORE::return( $self->{dt}->stringify );
}
# NOTE: DESTROY
DESTROY {};
# NOTE: AUTOLOAD
AUTOLOAD
{
my( $method ) = our $AUTOLOAD =~ /([^:]+)$/;
no overloading;
my $self = shift( @_ );
my $class = ref( $self ) || $self;
if( !ref( $self ) )
{
if( DateTime->can( $method ) )
{
my $rv = DateTime->$method( @_ );
if( Scalar::Util::blessed( $rv ) && $rv->isa( 'DateTime' ) )
{
return( $class->new( $rv ) );
}
else
{
return( $rv );
}
}
else
{
die( "Method ${method} unsupported by DateTime\n" );
}
}
die( "DateTime object is gone !\n" ) if( !ref( $self->{dt} ) );
no overloading;
my $dt = $self->{dt};
if( $dt->can( $method ) )
{
my $rv;
# try-catch
local $@;
eval
{
$rv = $dt->$method( @_ );
};
if( $@ )
{
return( $self->error( "Error trying to call DateTime::$method with arguments: '", join( "', '", @_ ), "': $@" ) );
}
return( $rv );
}
else
{
return( $self->error( "No method \"$method\" available in DateTime" ) );
}
};
# NOTE: package Module::Generic::DateTime::Interval
BEGIN
{
use strict;
use warnings;
use overload (
'""' => 'as_string',
'bool' => sub{1},
'+' => '__add_overload',
'-' => '__subtract_overload',
'*' => '__multiply_overload',
'<=>' => '__compare_overload',
'cmp' => '__compare_overload',
fallback => 1,
);
use DateTime;
# use Nice::Try;
use Scalar::Util ();
use Want;
};
sub new
{
my $this = shift( @_ );
my $dur = shift( @_ ) || return;
return( bless( { interval => $dur->clone } => ( ref( $this ) || $this ) )->init( @_ ) );
}
# This class does not convert to an HASH
sub as_hash { return( $_[0] ); }
sub as_string
{
my $self = shift( @_ );
return( $self->{interval}->in_units( 'seconds' ) );
}
sub dump
{
my $self = shift( @_ );
my @info = $self->{interval}->in_units( qw( years months weeks days hours minutes seconds nanoseconds ) );
my $tmpl = <<EOT;
Years ... %d
Months .. %d
Weeks ... %d
Days .... %d
Hours ... %d
Minutes . %d
Seconds . %d
EOT
return( sprintf( $tmpl, @info ) );
}
sub days : lvalue { return( shift->__set_get_unit( 'days', @_ ) ); }
sub hours : lvalue { return( shift->__set_get_unit( 'hours', @_ ) ); }
sub minutes : lvalue { return( shift->__set_get_unit( 'minutes', @_ ) ); }
sub months : lvalue { return( shift->__set_get_unit( 'months', @_ ) ); }
sub nanoseconds : lvalue { return( shift->__set_get_unit( 'nanoseconds', @_ ) ); }
sub seconds : lvalue { return( shift->__set_get_unit( 'seconds', @_ ) ); }
sub weeks : lvalue { return( shift->__set_get_unit( 'weeks', @_ ) ); }
sub years : lvalue { return( shift->__set_get_unit( 'years', @_ ) ); }
sub __add_overload
{
my( $self, $other, $swap ) = @_;
my $dur1 = $self->{interval};
$other = $self->_get_other( $other );
my $res;
if( !defined( $swap ) )
{
$dur1 += $other;
return( $self );
}
elsif( Scalar::Util::blessed( $other ) &&
( $other->isa( 'DateTime::Duration' ) || $other->isa( 'Module::Generic::DateTime::Interval' ) ) )
{
$other = $other->{interval} if( $other->isa( 'Module::Generic::DateTime::Interval' ) );
$res = $swap ? ( $other + $dur1 ) : ( $dur1 + $other );
return( $self->_make_my_own( $res ) );
}
elsif( !ref( $other ) || overload::Method( $other => '""' ) )
{
$other = $other + 0;
my $d = $dur1->in_units( 'seconds' );
my $n = $swap ? ( $other + $d ) : ( $d + $other );
$res = DateTime::Duration->new( seconds => $n );
return( $self->_make_my_own( $res ) );
}
else
{
die( "Usupported data '", ref( $other ), "' in subtraction\n" );
}
}
sub __compare_overload
{
my( $self, $other, $swap ) = @_;
my $d1 = $self->{interval};
my $d2 = $self->_get_other( $other );
# my $dt = DateTime->now;
( $d1, $d2 ) = ( $d2, $d1 ) if( $swap );
my $to_secs = sub
{
my $this = shift( @_ );
if( Scalar::Util::blessed( $this ) )
{
if( $this->isa( 'DateTime::Duration' ) )
{
return( $this->in_units( 'seconds' ) );
}
elsif( $this->isa( 'DateTime' ) )
{
return( $this->epoch );
}
elsif( $this->isa( 'Module::Generic::DateTime' ) )
{
return( $this->{dt}->epoch );
}
elsif( $this->isa( 'Module::Generic::DateTime::Duration' ) )
{
return( $this->{duration}->in_units( 'seconds' ) );
}
elsif( overload::Method( $this => '""' ) )
{
return( $this + 0 );
}
else
{
die( "Unsupported object '", ref( $this ), "'\n" );
}
}
else
{
return( $this + 0 );
}
};
# return( DateTime->compare(
# $dt->clone->add_duration( $d1 ),
# $dt->clone->add_duration( $d2 )
# ) );
my $d1_secs = $to_secs->( $d1 );
my $d2_secs = $to_secs->( $d2 );
return( $d1_secs <=> $d2_secs );
}
sub __multiply_overload
{
my( $self, $num, $swap ) = @_;
my @units = qw( months days minutes seconds nanoseconds );
if( "$num" =~ /^\d+$/ )
{
$num = int( "$num" );
## If $swap is undefined, it means an assignment operation like *=
my $clone = !defined( $swap ) ? $self->{interval} : $self->{interval}->clone;
$clone->multiply( $num );
return( !defined( $swap ) ? $self : $self->_make_my_own( $clone ) );
}
elsif( Scalar::Util::blessed( $num ) && $num->isa( 'Module::Generic::DateTime::Interval' ) )
{
my $clone = !defined( $swap ) ? $self->{interval} : $self->{interval}->clone;
foreach my $t ( @units )
{
$clone->{ $t } *= $num->{ $t };
}
$clone->_normalize_nanoseconds if( $clone->{nanoseconds} );
return( !defined( $swap ) ? $self : $self->_make_my_own( $clone ) );
}
else
{
return( $self );
}
}
sub __set_get_unit : lvalue
{
my $self = shift( @_ );
my $unit = shift( @_ );
my $dur = $self->{interval};
my $coderef = $dur->can( $unit );
my $update_value = sub
{
my $v = shift( @_ );
if( $unit eq 'years' )
{
$dur->{years} = $v;
my $p_months = $dur->{months};
if( $p_months > 12 )
{
my $n_years = int( $p_months / 12 );
$p_months -= ( 12 * $n_years );
$dur->{months} = ( $dur->{years} * 12 ) + $p_months;
}
}
elsif( $unit eq 'months' )
{
$dur->{months} = $v;
}
elsif( $unit eq 'weeks' )
{
$dur->{weeks} = $v;
if( $dur->{days} > 7 )
{
my $p_days = $dur->{days};
my $n_weeks = int( $p_days / 7 );
$p_days -= ( 7 * $n_weeks );
$dur->{days} = ( $dur->{weeks} * 7 ) + $p_days;
}
}
elsif( $unit eq 'days' )
{
$dur->{days} = ( $dur->{weeks} * 7 ) + $v;
}
elsif( $unit eq 'hours' )
{
$dur->{hours} = $v;
if( $dur->{minutes} > 60 )
{
my $p_minutes = $dur->{minutes};
my $n_hours = int( $p_minutes / 60 );
$p_minutes -= ( 60 * $n_hours );
$dur->{minutes} = ( $dur->{hours} * 60 ) + $p_minutes;
}
}
elsif( $unit eq 'minutes' )
{
$dur->{minutes} = ( $dur->{hours} * 60 ) + $v;
}
elsif( $unit eq 'seconds' )
{
$dur->{seconds} = $v;
}
elsif( $unit eq 'nanoseconds' )
{
$dur->{nanoseconds} = $v;
$self->_normalize_nanoseconds;
}
};
if( want( qw( LVALUE ASSIGN ) ) )
{
my( $v ) = want( 'ASSIGN' );
$update_value->( $v );
$coderef->( $dur );
return( $dur->{ $unit } );
}
else
{
if( @_ )
{
my $v = shift( @_ );
$update_value->( $v );
}
my $curr_v = $coderef->( $dur );
return( $curr_v ) if( want( 'LVALUE' ) );
rreturn( $curr_v );
}
return;
}
sub __subtract_overload
{
my( $self, $other, $swap ) = @_;
my $dur1 = $self->{interval};
$other = $self->_get_other( $other );
my $res;
if( !defined( $swap ) )
{
$dur1 -= $other;
return( $self );
}
elsif( Scalar::Util::blessed( $other ) &&
( $other->isa( 'DateTime::Duration' ) || $other->isa( 'Module::Generic::DateTime::Interval' ) ) )
{
$other = $other->{interval} if( $other->isa( 'Module::Generic::DateTime::Interval' ) );
$res = $swap ? ( $other - $dur1 ) : ( $dur1 - $other );
return( $self->_make_my_own( $res ) );
}
elsif( !ref( $other ) || overload::Method( $other => '""' ) )
{
$other = $other + 0;
my $d = $dur1->in_units( 'seconds' );
my $n = $swap ? ( $other - $d ) : ( $d - $other );
$res = DateTime::Duration->new( seconds => $n );
return( $self->_make_my_own( $res ) );
}
else
{
die( "Usupported data '", ref( $other ), "' in subtraction\n" );
}
}
sub _get_other
{
my( $self, $other ) = @_;
if( Scalar::Util::blessed( $other ) )
{
if( $other->isa( 'Module::Generic::DateTime' ) )
{
$other = $other->{dt};
}
elsif( $other->isa( 'Module::Generic::DateTime::Interval' ) )
{
$other = $other->{interval};
}
}
return( $other );
}
sub _make_my_own
{
my( $self, $res ) = @_;
if( Scalar::Util::blessed( $res ) &&
$res->isa( 'DateTime::Duration' ) )
{
return( $self->new( $res ) );
}
elsif( Scalar::Util::blessed( $res ) &&
$res->isa( 'DateTime' ) )
{
return( Module::Generic::DateTime->new( $res ) );
}
else
{
return( $res );
}
}
DESTROY
{
};
AUTOLOAD
{
my( $method ) = our $AUTOLOAD =~ /([^:]+)$/;
no overloading;
my $self = shift( @_ );
my $class = ref( $self ) || $self;
die( "DateTime::Duration object is gone !\n" ) if( !ref( $self->{interval} ) );
no overloading;
my $dur = $self->{interval};
if( $dur->can( $method ) )
{
my $rv;
# try-catch
local $@;
eval
{
$rv = $dur->$method( @_ );
};
if( $@ )
{
return( $self->error( "Error trying to call DateTime::Duration::$method with arguments: '", join( "', '", @_ ), "': $@" ) );
}
return( $rv );
}
else
{
return( $self->error( "No method \"$method\" available in DateTime::Duration" ) );
}
};
# NOTE: FREEZE is inherited
sub STORABLE_freeze { CORE::return( CORE::shift->FREEZE( @_ ) ); }
sub STORABLE_thaw { CORE::return( CORE::shift->THAW( @_ ) ); }
# NOTE: THAW is inherited
1;
# NOTE: POD
__END__
=encoding utf8
=head1 NAME
Module::Generic::DateTime - A DateTime wrapper for enhanced features
=head1 SYNOPSIS
use Module::Generic::DateTime;
my $dt = DateTime->new;
my $gdt = Module::Generic::DateTime->new( $dt );
# or directly will instantiate a default DateTime value based on DateTime->now
my $gdt = Module::Generic::DateTime->new;
# Now you can do operations that are not normally possible with DateTime
# Compare a dt object with a unix timestamp
if( $gdt > time() )
{
# do something
}
elsif( $gdt < '2020-03-01 07:12:10+0900' )
{
# do something
}
# and of course, comparison with other dt works as before
elsif( $gdt >= $dt )
{
# do something
}
# Remove 10 seconds from time object
$gdt -= 10;
# Add 5 seconds and get a new object
my $dt2 = $gdt + 5;
# Get the difference as an interval between two objects
my $interval = $dt1 - $dt2;
# DateTime::Duration are represented by Module::Generic::DateTime::Interval
# and extra manipulations are possible
# Add 7 seconds
$int += 7;
# Change the days
$int->days( 5 );
# or using lvalue
$int->days = 5;
# or multiply everything (years, months, weeks, days, hours, minutes, seconds and nanoseconds) in the interval by 2
$int *= 2
# Multiply one interval by another:
my $new_interval = $int1 * $int2;
# or multiply with assignment
$int1 *= $int2;
# Then add the interval to the datetime object
$dt += $int;
=head1 VERSION
v0.5.0
=head1 DESCRIPTION
L<Module::Generic::DateTime> is a thin wrapper around L<DateTime> to provide additional features as exemplified above.
It also enables the L<DateTime> object to be thawed and frozen and converted to L<JSON> with the respective methods C<STORABLE_freeze>, C<STORABLE_thaw>, C<TO_JSON>
All other method calls not in this API are passed to L<DateTime> using C<AUTOLOAD> with the added benefit that, if a method called triggers a fatal exception, it is caught using L<Nice::Try> try-catch block and an L<error|Module::Generic/error> is set and C<return> is returned instead.
=head1 CONSTRUCTOR
=head2 new
Provided with an optional L<DateTime> object and this returns a new instance of L<Module::Generic::DateTime>.
If no L<DateTime> object was provided, this will instantiate one implicitly and set the formatter to stringify it to an iso8601 string, such as: C<2022-03-08T14:22:10+0000>. By default the instantiated L<DateTime> object use the default time zone, which is C<GMT>. You can change the time zone afterward using L<DateTime/set_time_zone>:
$dt->set_time_zone( 'Asia/Tokyo' );
=head2 from_epoch
my $d = Module::Generic::DateTime->from_epoch( epoch => $unix_timestamp );
Instantiate a new L<Module::Generic::DateTime> using the L<DateTime> method C<from_epoch>. Any parameters are passed through to L<DateTime/from_epoch>
If a L<DateTime> error occurs, it will be caught and an L<error|Module::Generic/error> will be set and C<undef> will be returned.
=head2 now
my $d = Module::Generic::DateTime->now;
Instantiate a new L<Module::Generic::DateTime> using the L<DateTime> method C<now>. Any parameters are passed through to L<DateTime/now>
If a L<DateTime> error occurs, it will be caught and an L<error|Module::Generic/error> will be set and C<undef> will be returned.
=head1 METHODS
=head2 as_string
This is an alias to L</stringify>
=head2 datetime
Sets or gets the underlying L<DateTime> object.
=head2 op
This method is called to overload the following operations:
=over 4
=item * C<""> stringification
=item * C<bool>
=item * C<>> greater than
=item * C<>=> greater or equal than
=item * C<<> lower than
=item * C<<=> lower or equal than
=item * C<==> euqal
=item * C<!=> not equal
=item * C<-> minus
=item * C<+> plus
=back
=head2 op_minus_plus
This methods handles cases of overloading for C<minus> and C<plus>
=head1 SERIALISATION
=for Pod::Coverage FREEZE
=for Pod::Coverage STORABLE_freeze
=for Pod::Coverage STORABLE_thaw
=for Pod::Coverage THAW
=for Pod::Coverage TO_JSON
Serialisation by L<CBOR|CBOR::XS>, L<Sereal> and L<Storable::Improved> (or the legacy L<Storable>) is supported by this package. To that effect, the following subroutines are implemented: C<FREEZE>, C<THAW>, C<STORABLE_freeze> and C<STORABLE_thaw>
Additionally, upon loading L<Module::Generic::DateTime>, it will ensure the following L<DateTime> modules also have a C<FREEZE> and C<THAW> subroutines if not defined already: L<DateTime>, L<DateTime::TimeZone>, L<DateTime::TimeZone::OffsetOnly>, L<DateTime::Locale::FromData>, L<DateTime::Locale::Base>
=head1 SEE ALSO
L<Module::Generic>, L<Module::Generic::DateTime::Interval>, L<DateTime>, L<DateTime::Format::Strptime>, L<DatetTime::TimeZone>
=head1 AUTHOR
Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
=head1 COPYRIGHT & LICENSE
Copyright (c) 2000-2024 DEGUEST Pte. Ltd.
You can use, copy, modify and redistribute this package and associated
files under the same terms as Perl itself.
=cut