diff --git a/dist.ini b/dist.ini index 51b5af7..9f039e9 100644 --- a/dist.ini +++ b/dist.ini @@ -27,14 +27,14 @@ remove = PodVersion Test::More = 0.96 ; for runtime -perl = 5.010000 -DateTime = 0 -File::Slurp = 0 -Log::Any = 0 -Moo = 0 +perl = 5.010000 +DateTime = 0 +DateTime::Event::Recurrence = 0 +File::Slurp = 0 +Log::Any = 0 +Moo = 0 +String::Escape = 0 ; for example script: remind-due-todos Sub::Spec::CmdLine = 0.26 -; for example script: dump-org-structure -String::Escape = 0 diff --git a/lib/Org/Document.pm b/lib/Org/Document.pm index 6453235..955b92e 100644 --- a/lib/Org/Document.pm +++ b/lib/Org/Document.pm @@ -72,8 +72,8 @@ our $arg_re = qr/(?: '(? [^']*)' | (? \S+) ) /x; our $args_re = qr/(?: $arg_re (?:[ \t]+ $arg_re)*)/x; -my $tstamp_re = qr/(?:\[\d{4}-\d{2}-\d{2} \s+ [^\]]*\])/x; -my $act_tstamp_re = qr/(?:<\d{4}-\d{2}-\d{2} \s+ [^>]*>)/x; +my $tstamp_re = qr/(?:\[\d{4}-\d{2}-\d{2} [ ] [^\n\]]*\])/x; +my $act_tstamp_re = qr/(?:<\d{4}-\d{2}-\d{2} [ ] [^\n>]*>)/x; my $fn_name_re = qr/(?:[^ \t\n:\]]+)/x; my $text_re = qr( @@ -496,36 +496,44 @@ sub _add_text { $parent, $pass) : undef); } elsif ($+{trange}) { require Org::Element::TimeRange; + require Org::Element::Timestamp; $el = Org::Element::TimeRange->new( - _str => $+{trange}, document => $self, parent => $parent, - datetime1 => __parse_timestamp($+{trange_ts1}), - datetime2 => __parse_timestamp($+{trange_ts2}), ); + my $opts = {allow_event_duration=>0, allow_repeater=>0}; + $el->ts1(Org::Element::Timestamp->new( + document=>$self, parent=>$parent)); + $el->ts1->_parse_timestamp($+{trange_ts1}, $opts); + $el->ts2(Org::Element::Timestamp->new( + document=>$self, parent=>$parent)); + $el->ts2->_parse_timestamp($+{trange_ts2}, $opts); + $el->children([$el->ts1, $el->ts2]); } elsif ($+{tstamp}) { require Org::Element::Timestamp; $el = Org::Element::Timestamp->new( - _str=>$+{tstamp}, document => $self, parent => $parent, - datetime => __parse_timestamp($+{tstamp}), ); + $el->_parse_timestamp($+{tstamp}); } elsif ($+{act_trange}) { require Org::Element::TimeRange; + require Org::Element::Timestamp; $el = Org::Element::TimeRange->new( - _str=>$+{act_trange}, document => $self, parent => $parent, - is_active => 1, - datetime1 => __parse_timestamp($+{act_trange_ts1}), - datetime2 => __parse_timestamp($+{act_trange_ts2}), ); + my $opts = {allow_event_duration=>0, allow_repeater=>0}; + $el->ts1(Org::Element::Timestamp->new( + document=>$self, parent=>$parent)); + $el->ts1->_parse_timestamp($+{act_trange_ts1}, $opts); + $el->ts2(Org::Element::Timestamp->new( + document=>$self, parent=>$parent)); + $el->ts2->_parse_timestamp($+{act_trange_ts2}, $opts); + $el->children([$el->ts1, $el->ts2]); } elsif ($+{act_tstamp}) { require Org::Element::Timestamp; $el = Org::Element::Timestamp->new( - _str=>$+{act_tstamp}, document => $self, parent => $parent, - is_active => 1, - datetime => __parse_timestamp($+{act_tstamp}), ); + $el->_parse_timestamp($+{act_tstamp}); } elsif ($+{markup_start}) { require Org::Element::Text; $el = Org::Element::Text->new( @@ -718,23 +726,6 @@ sub _add_plain_text { push @{ $parent->children }, $el; } -# temporary place -sub __parse_timestamp { - require DateTime; - my ($ts) = @_; - $ts =~ /^(?:\[|<)?(\d{4})-(\d{2})-(\d{2}) \s - (?:\w{2,3} - (?:\s (\d{2}):(\d{2}))?)? - (?:\]|>)? - $/x - or die "Can't parse timestamp string: $ts"; - my %dt_args = (year => $1, month=>$2, day=>$3); - if (defined($4)) { $dt_args{hour} = $4; $dt_args{minute} = $5 } - my $res = DateTime->new(%dt_args); - $res or die "Invalid date: $ts"; - $res; -} - sub __split_tags { [$_[0] =~ /:([^:]+)/g]; } diff --git a/lib/Org/Element/TimeRange.pm b/lib/Org/Element/TimeRange.pm index b6a7f32..cc47411 100644 --- a/lib/Org/Element/TimeRange.pm +++ b/lib/Org/Element/TimeRange.pm @@ -7,23 +7,17 @@ extends 'Org::Element::Base'; =head1 ATTRIBUTES -=head2 datetime1 => DATETIME_OBJ +=head2 ts1 => TIMESTAMP ELEMENT =cut -has datetime1 => (is => 'rw'); +has ts1 => (is => 'rw'); -=head2 datetime2 => DATETIME_OBJ +=head2 ts2 => TIMESTAMP ELEMENT =cut -has datetime2 => (is => 'rw'); - -=head2 is_active => BOOL - -=cut - -has is_active => (is => 'rw'); +has ts2 => (is => 'rw'); =head1 METHODS @@ -36,13 +30,9 @@ sub as_string { my ($self) = @_; return $self->_str if $self->_str; join("", - $self->is_active ? "<" : "[", - $self->datetime1->ymd, " ", - # XXX Thu 11:59 - $self->is_active ? ">--<" : "]--[", - $self->datetime2->ymd, " ", - # XXX Thu 11:59 - $self->is_active ? ">" : "]", + $self->ts1->as_string, + "--", + $self->ts2->as_string ); } diff --git a/lib/Org/Element/Timestamp.pm b/lib/Org/Element/Timestamp.pm index c2aeaf2..f6186cd 100644 --- a/lib/Org/Element/Timestamp.pm +++ b/lib/Org/Element/Timestamp.pm @@ -13,6 +13,31 @@ extends 'Org::Element::Base'; has datetime => (is => 'rw'); +=head2 has_time => BOOL + +=cut + +has has_time => (is => 'rw'); + +=head2 event_duration => INT + +Event duration in seconds, e.g. for event timestamp like this: + + <2011-03-23 10:15-13:25> + +event_duration is 7200+600=7800 (2 hours 10 minutes). + +=cut + +has event_duration => (is => 'rw'); + +=head2 recurrence => DateTime::Event::Recurrence object + +=cut + +has recurrence => (is => 'rw'); +has _repeater => (is => 'rw'); # stores the raw repeater spec + =head2 is_active => BOOL =cut @@ -26,17 +51,116 @@ has is_active => (is => 'rw'); =cut +our @dow = (undef, qw(Mon Tue Wed Thu Fri Sat Sun)); + sub as_string { my ($self) = @_; return $self->_str if $self->_str; + my $dt = $self->datetime; + my ($hour2, $min2); + if ($self->event_duration) { + my $hour = $dt->hour; + my $min = $dt->minute; + my $mins = $self->event_duration / 60; + $min2 = $min + $mins; + my $hours = int ($min2 / 60); + $hour2 = $hour + $hours; + $min2 = $min2 % 60; + } join("", $self->is_active ? "<" : "[", - $self->datetime->ymd, " ", - # XXX Thu 11:59 + $dt->ymd, " ", + $dow[$dt->day_of_week], + $self->has_time ? ( + " ", + sprintf("%02d:%02d", $dt->hour, $dt->minute), + defined($hour2) ? ( + "-", + sprintf("%02d:%02d", $hour2, $min2), + ) : (), + $self->_repeater ? ( + " +", + $self->_repeater, + ) : (), + ) : (), $self->is_active ? ">" : "]", ); } +sub _parse_timestamp { + require DateTime; + require DateTime::Event::Recurrence; + my ($self, $str, $opts) = @_; + $opts //= {}; + $opts->{allow_event_duration} //= 1; + $opts->{allow_repeater} //= 1; + + $str =~ /^(? \[|<) + (? \d{4})-(? \d{2})-(? \d{2}) \s + (?: + (? \w{2,3}) + (?:\s + (? \d{2}):(? \d{2}) + (?:- + (? + (? \d{2}):(? \d{2})) + )? + )? + (?:\s\+ + (? + (? \d+) + (? [dwmy]) + ) + )? + )? + (? \]|>) + $/x + or die "Can't parse timestamp string: $str"; + die "Duration not allowed in timestamp: $str" + if !$opts->{allow_event_duration} && $+{event_duration}; + die "Repeater ($+{repeater}) not allowed in timestamp: $str" + if !$opts->{allow_repeater} && $+{repeater}; + + $self->is_active($+{open_bracket} eq '<' ? 1:0) + unless defined $self->is_active; + + if ($+{event_duration} && !defined($self->event_duration)) { + $self->event_duration( + ($+{hour2}-$+{hour})*3600 + + ($+{min2} -$+{min} )*60 + ); + } + + if ($+{repeater} && !$self->recurrence) { + my $r; + my $i = $+{repeater_interval}; + my $u = $+{repeater_unit}; + if ($u eq 'd') { + $r = DateTime::Event::Recurrence->daily(interval=>$i); + } elsif ($u eq 'w') { + $r = DateTime::Event::Recurrence->weekly(interval=>$i); + } elsif ($u eq 'm') { + $r = DateTime::Event::Recurrence->monthly(interval=>$i); + } elsif ($u eq 'y') { + $r = DateTime::Event::Recurrence->yearly(interval=>$i); + } else { + die "BUG: Unknown repeater unit $u in timestamp $str"; + } + $self->recurrence($r); + $self->_repeater($+{repeater}); + } + + my %dt_args = (year => $+{year}, month=>$+{mon}, day=>$+{day}); + if (defined($+{hour})) { + $dt_args{hour} = $+{hour}; + $dt_args{minute} = $+{min}; + $self->has_time(1); + } else { + $self->has_time(0); + } + $self->datetime(DateTime->new(%dt_args)); +} + 1; __END__ diff --git a/t/timerange.t b/t/timerange.t index c79fa67..a0967c3 100644 --- a/t/timerange.t +++ b/t/timerange.t @@ -37,9 +37,29 @@ _ my %args = @_; my $doc = $args{result}; my $elems = $args{elements}; - ok( $elems->[0]->is_active, "tr[0] is_active"); - ok(!$elems->[3]->is_active, "tr[3] !is_active"); + ok( $elems->[0]->ts1->is_active, "tr[0] is_active"); + ok(!$elems->[3]->ts1->is_active, "tr[3] !is_active"); }, ); +test_parse( + name => 'event duration not allowed in timerange', + filter_elements => sub { + $_[0]->isa('Org::Element::TimeRange') }, + doc => <<'_', +<2011-03-23 Wed 11:28-12:00>--<2011-03-24 Thu> +_ + dies => 1, +); + +test_parse( + name => 'repeater not allowed in timerange', + filter_elements => sub { + $_[0]->isa('Org::Element::TimeRange') }, + doc => <<'_', +<2011-03-23 Wed +1w>--<2011-03-24 Thu> +_ + dies => 1, +); + done_testing(); diff --git a/t/timestamp.t b/t/timestamp.t index 955ec7c..d91484e 100644 --- a/t/timestamp.t +++ b/t/timestamp.t @@ -35,10 +35,65 @@ _ my $doc = $args{result}; my $elems = $args{elements}; is(DateTime->compare(DateTime->new(year=>2011, month=>3, day=>16), - $elems->[0]->datetime), 0, "ts[0] datetime"); + $elems->[0]->datetime), 0, "ts[0] datetime") + or diag("datetime=".$elems->[0]->datetime); + + is( $elems->[0]->as_string, "<2011-03-16 Wed>", "ts[0] as_string"); + is( $elems->[1]->as_string, "<2011-03-16 Wed>", "ts[1] as_string"); + is( $elems->[2]->as_string, "<2011-03-16 Wed 01:23>", + "ts[2] as_string"); + is( $elems->[3]->as_string, "[2011-03-23 Wed]", + "ts[2] as_string"); + ok( $elems->[0]->is_active, "ts[0] is_active"); ok(!$elems->[3]->is_active, "ts[3] !is_active"); }, ); +test_parse( + name => 'event duration', + filter_elements => sub { + $_[0]->isa('Org::Element::Timestamp') }, + doc => <<'_', +[2011-03-23 Wed 10:12-11:23] +_ + num => 1, + test_after_parse => sub { + my %args = @_; + my $doc = $args{result}; + my $elems = $args{elements}; + my $ts = $elems->[0]; + is(DateTime->compare(DateTime->new(year=>2011, month=>3, day=>23, + hour=>10, minute=>12), + $ts->datetime), 0, "datetime") + or diag("datetime=".$ts->datetime); + is($elems->[0]->event_duration, 1*3600+11*60, "event_duration"); + }, +); + +test_parse( + name => 'repeater', + filter_elements => sub { + $_[0]->isa('Org::Element::Timestamp') }, + doc => <<'_', +[2011-03-23 Wed 10:12 +1d] +[2011-03-23 Wed 10:12-11:23 +2w] +[2011-03-23 Wed +3m] +[2011-03-23 Wed +4y] +_ + num => 4, + test_after_parse => sub { + my %args = @_; + my $doc = $args{result}; + my $elems = $args{elements}; + is($elems->[0]->_repeater, "1d", "[0] _repeater"); + is($elems->[1]->_repeater, "2w", "[0] _repeater"); + is($elems->[2]->_repeater, "3m", "[0] _repeater"); + is($elems->[3]->_repeater, "4y", "[0] _repeater"); + + ok($elems->[0]->recurrence->isa('DateTime::Set::ICal'), + "[0] recurrence"); + }, +); + done_testing(); diff --git a/todo.org b/todo.org index c991455..0c9777a 100644 --- a/todo.org +++ b/todo.org @@ -1,25 +1,11 @@ * todo ** Element::Base -*** DONE a method to walk elements -option to do breadth-first or depth-first. this should be used for other walk -methods, e.g. find_next_heading(), find_next_subheading() *** TODO headline() return the headline this element is in. *** TODO get_tags() return list of tags including inherited tags -*** DONE get_property() -return property, including inherited ones *** TODO set_property() - should create a properties drawer if necessary -** TODO parse sexp entries? -e.g. - - ** Class 7:00pm-9:00pm - <%%(and (= 1 (calendar-day-of-week date)) (diary-block 2 16 2009 4 20 2009))> - - * Monthly meeting - <%%(diary-float t 3 3)> - ** table *** TODO format() - this should undef/replace ._raw @@ -59,7 +45,15 @@ format() needs to worry about this (i prefer the latter). ** target ** radio target ** timestamp & time range -*** TODO parse repeater interval +*** TODO parse sexp entries? +e.g. + + ** Class 7:00pm-9:00pm + <%%(and (= 1 (calendar-day-of-week date)) (diary-block 2 16 2009 4 20 2009))> + + * Monthly meeting + <%%(diary-float t 3 3)> + ** plain lists (ordered, unordered, description) ** headline *** TODO what constitutes a valid tag?