# DateTags.pl # Movable Type plugin tags for date-based output # by Kevin Shay # http://www.staggernation.com/mtplugins/ # last modified February 17, 2005 use strict; package MT::Plugin::DateTagsA; use vars qw( $VERSION ); $VERSION = '2.3'; use MT; use MT::Template::Context; eval{ require MT::Plugin }; unless ($@) { my $plugin = { name => "DateTags $VERSION", description => 'Enhance your MT site with a variety of date-related tags.', doc_link => 'http://www.staggernation.com/mtplugins/DateTags' }; MT->add_plugin(new MT::Plugin($plugin)); } MT::Template::Context->add_tag('EnglishOrdinal' => sub{&_hdlr_english_ordinal;}); MT::Template::Context->add_tag('DeltaDays' => sub{&_hdlr_delta_days;}); MT::Template::Context->add_conditional_tag('IfDaysOfWeek' => sub{&_hdlr_if_days_of_week;}); MT::Template::Context->add_conditional_tag('IfDaysOfMonth' => sub{&_hdlr_if_days_of_month;}); MT::Template::Context->add_conditional_tag('IfMonths' => sub{&_hdlr_if_months;}); MT::Template::Context->add_conditional_tag('IfDateWithin' => sub{&_hdlr_if_date_within;}); MT::Template::Context->add_conditional_tag('IfDateEqual' => sub{&_hdlr_if_date_equal;}); MT::Template::Context->add_conditional_tag('IfDateNotEqual' => sub{&_hdlr_if_date_not_equal;}); MT::Template::Context->add_conditional_tag('IfDateBefore' => sub{&_hdlr_if_date_before;}); MT::Template::Context->add_conditional_tag('IfDateAfter' => sub{&_hdlr_if_date_after;}); MT::Template::Context->add_conditional_tag('IfDateBeforeOrEqual' => sub{&_hdlr_if_date_before_or_equal;}); MT::Template::Context->add_conditional_tag('IfDateAfterOrEqual' => sub{&_hdlr_if_date_after_or_equal;}); MT::Template::Context->add_container_tag('DateLoop' => sub{&_hdlr_date_loop;}); MT::Template::Context->add_tag('DateLoopDate' => sub{&_hdlr_date_loop_date;}); MT::Template::Context->add_container_tag('DateRange' => sub{&_hdlr_date_range;}); MT::Template::Context->add_tag('DateRangeStartDate' => sub{&_hdlr_date_range_start_date;}); MT::Template::Context->add_tag('DateRangeEndDate' => sub{&_hdlr_date_range_end_date;}); MT::Template::Context->add_conditional_tag('IfEntries' => sub{&_hdlr_if_entries;}); MT::Template::Context->add_conditional_tag('IfNoEntries' => sub{&_hdlr_if_no_entries;}); MT::Template::Context->add_container_tag('NextNEntries' => sub{&_hdlr_next_n_entries;}); MT::Template::Context->add_container_tag('PrevNEntries' => sub{&_hdlr_prev_n_entries;}); MT::Template::Context->add_conditional_tag('IfNEntries' => sub{&_hdlr_if_n_entries;}); MT::Template::Context->add_tag('NEntriesCount' => sub{&_hdlr_n_entries_count;}); # for backwards compatibility MT::Template::Context->add_conditional_tag('DateLoopIfEntries' => sub{&_hdlr_if_entries;}); MT::Template::Context->add_conditional_tag('DateLoopIfNoEntries' => sub{&_hdlr_if_no_entries;}); use DateTags; use MT::Util qw ( format_ts ); # keep track of whether we've loaded Date::Calc my $datecalc = 0; sub dt_load_datecalc { # instead of use()ing Date::Calc, load it the first time it's needed # (so it'll only load if one of the tags is actually called) require Date::Calc; import Date::Calc qw( Date_to_Days Day_of_Week Delta_Days English_Ordinal check_date Add_Delta_Days ); return 1; } sub _hdlr_english_ordinal { my ($ctx, $args) = @_; $datecalc ||= dt_load_datecalc(); (my $number = $args->{'number'}) || return $ctx->error('No number passed.'); $number = dt_build_value($ctx, $number); return English_Ordinal($number); } sub _hdlr_if_days_of_week { my ($ctx, $args, $cond) = @_; $datecalc ||= dt_load_datecalc(); my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); return $ctx->error('No days passed') unless defined($args->{'days'}); my ($y, $m, $d) = unpack('A4A2A2', dt_get_ts($ctx, $args)); my $dow = Day_of_Week($y, $m, $d); return 0 unless ($args->{'days'} =~ /$dow/); my $proceed = 1; if (defined($args->{'n'})) { $proceed = 0; for (split(/[ ,]/, $args->{'n'})) { my $nwomy = dt_nwomy($_, $dow, $m, $y); if ($nwomy == $d) { $proceed = 1; last; } } } return $proceed; } sub _hdlr_if_days_of_month { my ($ctx, $args, $cond) = @_; my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); return $ctx->error('No days passed') unless defined($args->{'days'}); my $d = substr(dt_get_ts($ctx, $args), 6, 2); my %days = map { sprintf("%02d", $_) => 1 } split(/[ ,]/, $args->{'days'}); return $days{$d} ? 1 : 0; } sub _hdlr_if_months { my ($ctx, $args, $cond) = @_; my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); return $ctx->error('No months passed') unless defined($args->{'months'}); my $m = substr(dt_get_ts($ctx, $args), 4, 2); my %months = map { sprintf("%02d", $_) => 1 } split(/[ ,]/, $args->{'months'}); return $months{$m} ? 1 : 0; } sub _hdlr_if_date_within { my ($ctx, $args, $cond) = @_; $datecalc ||= dt_load_datecalc(); my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); my (%now, %start, %end); @now{qw(y m d)} = unpack('A4A2A2', dt_get_ts($ctx, $args)); dt_get_start_end($ctx, $args, \%start, \%end); (Delta_Days(@start{qw(y m d)}, @end{qw(y m d)}) < 0) && return $ctx->error('Ending date before starting date'); my $within = ((dt_days(\%now) >= dt_days(\%start)) && (dt_days(\%now) <= dt_days(\%end))); return ($within xor defined($args->{'not'})) ? 1 : 0; } sub _hdlr_delta_days { my ($ctx, $args) = @_; $datecalc ||= dt_load_datecalc(); my $delta = dt_delta_days($ctx, $args); if (defined($args->{'adjust'})) { $delta += $args->{'adjust'}; } if (($delta < 0) && !defined($args->{'negative'})) { $delta = abs($delta); } my $thou = defined($args->{'thousands'}) ? $args->{'thousands'} : ''; if (defined($args->{'plural'})) { return (abs($delta) == 1) ? '' : $args->{'plural'}; } elsif (defined($args->{'ordinal'})) { my $ord = English_Ordinal($delta); if ($thou) { # we want "1,234th" return dt_insert_commas($delta, $thou) . substr($ord, length($ord) - 2); } else { return $ord; } } else { return $thou ? dt_insert_commas($delta, $thou) : $delta; } } sub _hdlr_if_date_equal { my ($ctx, $args, $cond) = @_; return dt_date_compare_hdlr($ctx, $args, $cond, '=='); } sub _hdlr_if_date_not_equal { my ($ctx, $args, $cond) = @_; return dt_date_compare_hdlr($ctx, $args, $cond, '!='); } sub _hdlr_if_date_before { my ($ctx, $args, $cond) = @_; return dt_date_compare_hdlr($ctx, $args, $cond, '>'); } sub _hdlr_if_date_after { my ($ctx, $args, $cond) = @_; return dt_date_compare_hdlr($ctx, $args, $cond, '<'); } sub _hdlr_if_date_before_or_equal { my ($ctx, $args, $cond) = @_; return dt_date_compare_hdlr($ctx, $args, $cond, '>='); } sub _hdlr_if_date_after_or_equal { my ($ctx, $args, $cond) = @_; return dt_date_compare_hdlr($ctx, $args, $cond, '<='); } sub _hdlr_next_n_entries { return dt_n_entries('ascend', @_); } sub _hdlr_prev_n_entries { return dt_n_entries('descend', @_); } sub dt_n_entries { my ($direction, $ctx, $args, $cond) = @_; return $ctx->error('No "n" value passed') unless ($args->{'n'}); require MT::Entry; my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); my $ts = dt_get_ts($ctx, $args); my $column = $args->{'modified_on'} ? 'modified_on' : 'created_on'; my @entries = MT::Entry->load({ blog_id => $ctx->stash('blog')->id, status => MT::Entry::RELEASE() }, { sort => $column, start_val => $ts, direction => $direction, limit => $args->{'n'} }); local $ctx->{__stash}{'entries'} = \@entries; local $ctx->{__stash}{'datetags_n_entries'} = scalar @entries; defined(my $text = $builder->build($ctx, $tokens, $cond)) || return $ctx->error($ctx->errstr); return $text; } sub _hdlr_if_n_entries { my ($ctx, $args, $cond) = @_; return '' unless $ctx->stash('datetags_n_entries'); defined(my $text = $ctx->stash('builder')->build($ctx, $ctx->stash('tokens'), $cond)) || return $ctx->error($ctx->errstr); return $text; } sub _hdlr_n_entries_count { my ($ctx, $args, $cond) = @_; return $ctx->error('Not called from within MTNextNEntries or MTPrevNEntries container') unless defined(my $count = $ctx->stash('datetags_n_entries')); return $count; } sub _hdlr_date_loop { my ($ctx, $args, $cond) = @_; $datecalc ||= dt_load_datecalc(); require MT::Entry; my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); my (%start, %end); dt_get_start_end($ctx, $args, \%start, \%end); my $delta = Delta_Days(@start{qw(y m d)}, @end{qw(y m d)}); my $text = ''; my $i = 0; # loop through dates. we need to loop at least once, and include # both the start and end dates. { my $ymd = sprintf("%04d%02d%02d", Add_Delta_Days(@start{qw(y m d)}, $i)); my $start_ts = "${ymd}000000"; my $end_ts = "${ymd}235959"; my @entries = MT::Entry->load({ blog_id => $ctx->stash('blog')->id, status => MT::Entry::RELEASE(), created_on => [ $start_ts, $end_ts ] }, { range => { created_on => 1 } }); # workaround for MT 2.x bug when loading entries by range under DBM @entries = map { (substr($_->created_on, 0, 8) eq $ymd) ? $_ : () } @entries; local $ctx->{__stash}{'dateloop_ts'} = $start_ts; local $ctx->{__stash}{'entries'} = \@entries; defined(my $iter = $builder->build($ctx, $tokens, $cond)) || return $ctx->error($ctx->errstr); $text .= $iter; last if ($i == $delta); ($delta > 0) ? $i++ : $i--; redo; } return $text; } sub _hdlr_date_loop_date { my ($ctx, $args) = @_; (my $ts = $ctx->stash('dateloop_ts')) || return $ctx->error('Not called from within MTDateLoop container'); return format_ts($args->{'format'}, $ts, $ctx->stash('blog'), $args->{'language'}); } sub _hdlr_date_range { my ($ctx, $args, $cond) = @_; $datecalc ||= dt_load_datecalc(); require MT::Entry; my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); my (%start, %end); dt_get_start_end($ctx, $args, \%start, \%end); my $start_ts = sprintf("%04d%02d%02d000000", @start{qw(y m d)}); my $end_ts = sprintf("%04d%02d%02d235959", @end{qw(y m d)}); my @entries = MT::Entry->load({ blog_id => $ctx->stash('blog')->id, status => MT::Entry::RELEASE(), created_on => [ $start_ts, $end_ts ] }, { range => { created_on => 1 } }); # workaround for MT 2.x bug when loading entries by range under DBM @entries = map { ($_->created_on le $end_ts) ? $_ : () } @entries; local $ctx->{__stash}{'daterange_start_ts'} = $start_ts; local $ctx->{__stash}{'daterange_end_ts'} = $end_ts; local $ctx->{__stash}{'entries'} = \@entries; defined(my $text = $builder->build($ctx, $tokens, $cond)) || return $ctx->error($ctx->errstr); return $text; } sub _hdlr_date_range_start_date { my ($ctx, $args) = @_; (my $ts = $ctx->stash('daterange_start_ts')) || return $ctx->error('Not called from within MTDateRange container'); return format_ts($args->{'format'}, $ts, $ctx->stash('blog'), $args->{'language'}); } sub _hdlr_date_range_end_date { my ($ctx, $args) = @_; (my $ts = $ctx->stash('daterange_end_ts')) || return $ctx->error('Not called from within MTDateRange container'); return format_ts($args->{'format'}, $ts, $ctx->stash('blog'), $args->{'language'}); } sub _hdlr_if_entries { my ($ctx, $args, $cond) = @_; return dt_if_entries($ctx, $args, $cond, 1); } sub _hdlr_if_no_entries { my ($ctx, $args, $cond) = @_; return dt_if_entries($ctx, $args, $cond, 0); } sub dt_if_entries { my ($ctx, $args, $cond, $want_entries) = @_; my $entries = $ctx->stash('entries'); my $has_entries = ($entries && @$entries) ? 1 : 0; return '' unless ($has_entries == $want_entries); defined(my $text = $ctx->stash('builder')->build($ctx, $ctx->stash('tokens'), $cond)) || return $ctx->error($ctx->errstr); return $text; } sub dt_get_start_end { my ($ctx, $args, $start, $end) = @_; my %flds = ('start' => $start, 'end' => $end); for (keys %flds) { return $ctx->error("No $_ date passed") unless ($args->{$_}); (my $ts = dt_build_date($ctx, $args, $args->{$_})) || return $ctx->error("Invalid $_ date"); @{$flds{$_}}{qw(y m d)} = unpack('A4A2A2', $ts); if (my $adj = $args->{"${_}_adjust"}) { @{$flds{$_}}{qw(y m d)} = Add_Delta_Days(@{$flds{$_}}{qw(y m d)}, $adj); } } } sub dt_date_compare_hdlr { my ($ctx, $args, $cond, $op) = @_; my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); my $text = ''; my $delta = dt_delta_days($ctx, $args); return eval("$delta $op 0") ? 1 : 0; } sub dt_delta_days { my ($ctx, $args) = @_; $datecalc ||= dt_load_datecalc(); return $ctx->error('No target date passed') unless $args->{'target'}; my (%date, %target); check_date(@date{qw(y m d)} = unpack('A4A2A2', dt_get_ts($ctx, $args))) || return $ctx->error('Invalid base date'); check_date(@target{qw(y m d)} = unpack('A4A2A2', dt_build_date($ctx, $args, $args->{'target'}))) || return $ctx->error('Invalid target date'); return Delta_Days(@date{qw(y m d)}, @target{qw(y m d)}); } sub dt_days { # passed a ref to a date hash with keys (y, m, d), # return the absolute "days" (since 1/1/1) value for that date my ($date) = @_; return Date_to_Days(@{$date}{qw(y m d)}); } sub dt_insert_commas { # put commas in a large number my ($num, $comma) = @_; return $num if (abs($num) < 1000); $comma ||= ','; $num = reverse($num); $num =~ s/(\d{3})(?=\d)(?!\d*\.)/$1$comma/g; $num = scalar reverse($num); return $num; } 1;