The Perl Advent Calendar needs more articles for 2022. Submit your idea today!
use 5.008;
use strict;
use warnings;

package Types::DateTime;

our $AUTHORITY = 'cpan:TOBYINK';
our $VERSION   = '0.002';

use DateTime;
use DateTime::Duration;
use DateTime::Locale;
use DateTime::TimeZone;

use Module::Runtime qw( use_module );

use Type::Library -base, -declare => qw(
	DateTime Duration TimeZone Locale Now
	DateTimeWithZone DateTimeUTC
);
use Types::Standard qw( Num Str HashRef InstanceOf );
use Type::Utils;

# This stuff for compat with MooseX::Types::DateTime

class_type(DateTime, { class => 'DateTime' });
class_type(Duration, { class => 'DateTime::Duration' });
class_type(TimeZone, { class => 'DateTime::TimeZone' });
declare Locale,
	as InstanceOf['DateTime::Locale::root','DateTime::Locale::FromData'];
enum(Now, ['now']);

coerce DateTime,
	from Num,     q{ 'DateTime'->from_epoch(epoch => $_) },
	from HashRef, q{ exists($_->{epoch}) ? 'DateTime'->from_epoch(%$_) : 'DateTime'->new(%{$_}) },
	from Now,     q{ 'DateTime'->now },
	from InstanceOf['DateTime::Tiny'], q{ $_->DateTime };

coerce Duration,
	from Num,     q{ 'DateTime::Duration'->new(seconds => $_) },
	from HashRef, q{ 'DateTime::Duration'->new(%{$_}) };

coerce TimeZone,
	from Str,     q{ 'DateTime::TimeZone'->new(name => $_) };

coerce Locale,
	from InstanceOf['Locale::Maketext'], q{ 'DateTime::Locale'->load($_->language_tag) },
	from Str,     q{ 'DateTime::Locale'->load($_) };

# Time zone stuff

declare DateTimeWithZone,
	as         DateTime,
	coercion   => 1,  # inherit coercions
	where      {          not($_ ->time_zone->is_floating)   },
	inline_as  { (undef, "not($_\->time_zone->is_floating)") },
	constraint_generator => sub {
		my $zone = TimeZone->assert_coerce(shift);
		sub { $_[0]->time_zone eq $zone };
	},
	coercion_generator => sub {
		my $parent = shift;
		my $child  = shift;
		my $zone   = TimeZone->assert_coerce(shift);
		
		my $c = 'Type::Coercion'->new(type_constraint => $child);
		$c->add_type_coercions(
			$parent->coercibles, sub {
				my $dt = DateTime->coerce($_);
				return $_ unless DateTime->check($dt);
				$dt->set_time_zone($zone);
				return $dt;
			},
		);
		$c;
	};

declare DateTimeUTC, as DateTimeWithZone['UTC'], coercion => 1;

# Stringy coercions. No sugar for this stuff ;-)

__PACKAGE__->meta->add_coercion({
	name               => 'Format',
	type_constraint    => DateTime,
	coercion_generator => sub {
		my ($self, $target, $format) = @_;
		$format = use_module("DateTime::Format::$format")->new
			unless ref($format);
		
		my $timezone;
		if ($target->is_a_type_of(DateTimeWithZone))
		{
			my ($paramd_type) = grep {
				$_->is_parameterized and $_->parent==DateTimeWithZone
			} ($target, $target->parents);
			$timezone = TimeZone->assert_coerce($paramd_type->type_parameter)
				if $paramd_type;
		}
		
		return (
			Str,
			sub {
				my $dt = eval { $format->parse_datetime($_) };
				return $_ unless $dt;
				$dt->set_time_zone($timezone) if $timezone;
				$dt;
			},
		);
	},
});

__PACKAGE__->meta->add_coercion({
	name               => 'Strftime',
	type_constraint    => Str,
	coercion_generator => sub {
		my ($self, $target, $format) = @_;
		my $format_quoted = B::perlstring($format);
		return (
			DateTime->coercibles,
			qq{\$_->strftime($format_quoted)},
		);
	},
});

__PACKAGE__->meta->add_coercion({
	name               => 'ToISO8601',
	type_constraint    => Str,
	type_coercion_map  => [
		DateTime->coercibles,
		q{$_->iso8601},
	],
});

1;

__END__

=pod

=encoding utf-8

=for stopwords datetime timezone

=head1 NAME

Types::DateTime - type constraints and coercions for datetime objects

=head1 SYNOPSIS

   package FroobleGala;
   
   use Moose;
   use Types::DateTime -all;
   
   has start_date => (
      is      => 'ro',
      isa     => DateTimeUTC->plus_coercions( Format['ISO8601'] ),
      coerce  => 1,
   );

=head1 DESCRIPTION

L<Types::DateTime> is a type constraint library suitable for use with
L<Moo>/L<Moose> attributes, L<Kavorka> sub signatures, and so forth.

=head2 Types

This module provides some type constraints broadly compatible with
those provided by L<MooseX::Types::DateTime>, plus a couple of extra
type constraints.

=over

=item C<DateTime>

A class type for L<DateTime>. Coercions from:

=over

=item from C<Num>

Uses L<DateTime/from_epoch>. Floating values will be used for sub-second
precision, see L<DateTime> for details.

=item from C<HashRef>

Calls L<DateTime/new> or L<DateTime/from_epoch> as appropriate, passing
the hash as arguments.

=item from C<Now>

Uses L<DateTime/now>.

=item from C<< InstanceOf['DateTime::Tiny'] >>

Inflated using L<DateTime::Tiny/DateTime>.

=back

=item C<Duration>

A class type for L<DateTime::Duration>. Coercions from:

=over

=item from C<Num>

Uses L<DateTime::Duration/new> and passes the number as the C<seconds>
argument.

=item from C<HashRef>

Calls L<DateTime::Duration/new> with the hash entries as arguments.

=back

=item C<Locale>

A class type for L<DateTime::Locale>. Coercions from:

=over

=item from C<Str>

The string is treated as a language tag (e.g. C<en> or C<he_IL>) and
given to L<DateTime::Locale/load>.

=item from C<< InstanceOf['Locale::Maketext'] >>

The C<Locale::Maketext/language_tag> attribute will be used with
L<DateTime::Locale/load>.

=back

=item C<TimeZone>

A class type for L<DateTime::TimeZone>. Coercions from:

=over

=item from C<Str>

Treated as a time zone name or offset. See L<DateTime::TimeZone/USAGE>
for more details on the allowed values.

Delegates to L<DateTime::TimeZone/new> with the string as the C<name>
argument.

=back

=item C<Now>

Type constraint with only one allowed value, the string "now".

This is exported for compatibility with L<MooseX::Types::DateTime>, which
exports such a constraint, even though it is not documented.

=item C<DateTimeWithZone>

A subtype of C<DateTime> for objects with a defined (non-floating) time
zone.

This type constraint inherits its coercions from C<DateTime>.

=item C<< DateTimeWithZone[`a] >>

The C<DateTimeWithZone> type constraint may be parameterized with a
L<DateTime::TimeZone> object, or a string that can be coerced into one.

   has start_date => (
      is      => 'ro',
      isa     => DateTimeWithZone['Europe/London'],
      coerce  => 1,
   );

This type constraint inherits its coercions from C<DateTime>, and will
additionally call L<DateTime/set_time_zone> to shift objects into the
correct timezone.

=item C<DateTimeUTC>

Shortcut for C<< DateTimeWithZone["UTC"] >>.

=back

=head2 Named Coercions

It is hoped that Type::Tiny will help avoid the proliferation of
modules like L<MooseX::Types::DateTimeX>,
L<MooseX::Types::DateTime::ButMaintained>, and
L<MooseX::Types::DateTime::MoreCoercions>. It makes it very easy to add
coercions to a type constraint at the point of use:

   has start_date => (
      is      => 'ro',
      isa     => DateTime->plus_coercions(
         InstanceOf['MyApp::DT'] => sub { $_->to_DateTime }
      ),
      coerce  => 1,
   );

Even easier, this module exports some named coercions.

=over

=item C<< Format[`a] >>

May be passed an object providing a C<parse_datetime> method, or a
class name from the C<< DateTime::Format:: >> namespace (upon which
C<new> will be called).

For example:

   DateTime->plus_coercions( Format['ISO8601'] )

Or:

   DateTimeUTC->plus_coercions(
      Format[
         DateTime::Format::Natural->new(lang => 'en')
      ]
   )

=item C<< Strftime[`a] >>

A pattern for serializing a DateTime object into a string using
L<DateTime/strftime>.

   Str->plus_coercions( Strftime['%a %e %b %Y'] );

=item C<ToISO8601>

A coercion for serializing a DateTime object into a string using
L<DateTime/iso8601>.

   Str->plus_coercions( ToISO8601 );

=back

=head1 BUGS

Please report any bugs to
L<http://rt.cpan.org/Dist/Display.html?Queue=Types-DateTime>.

=head1 SEE ALSO

L<MooseX::Types::DateTime>,
L<Type::Tiny::Manual>,
L<DateTime>,
L<DateTime::Duration>,
L<DateTime::Locale>,
L<DateTime::TimeZone>.

=head1 AUTHOR

Toby Inkster E<lt>tobyink@cpan.orgE<gt>.

=head1 COPYRIGHT AND LICENCE

This software is copyright (c) 2014, 2017 by Toby Inkster.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.