#!/usr/local/bin/perl # FILE %usr/unixonly/CPAN/hp200lx-db-0.04/DB/recurrence.pm # # handling of recurance rules in HP200LX ADB database # # T2D: # + more precise perl representation # + mapping HP200LX binary recurrence # + mapping vCalendar recurrence format # # written: 1998-09-20 # latest update: 2001-03-11 2:21:16 # $Id: $ # package HP200LX::DB::recurrence; use strict; use vars qw($VERSION @ISA @EXPORT_OK); use Exporter; use HP200LX::DB qw(fmt_date fmt_time pack_date hex_dump); $VERSION= '0.09'; @ISA= qw(Exporter); @EXPORT_OK= qw(:all new print_recurrence_status ); # ---------------------------------------------------------------------------- my $no_val= 65535; # NIL, empty list, -1 etc. my @BITS= ( 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, ); my @RECURRENCE_TEXT= ( 'never', 'daily', 'weekly', 'monthly', 'yearly', 'special' ); my @RECURRENCE_MONTH_GERMAN= ( 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' ); my @RECURRENCE_MONTH= ( 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ); my @RECURRENCE_WDAY= ( 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', ); my @RECURRENCE_DAY= ( '1st', '2nd', '3rd', '4th', 'last', ); my %RECURRENCE_XAPIA= ( 0 => 'U', 1 => 'N', 2 => 'D', 4 => 'W', 8 => 'M', 16 => 'Y', 32 => 'S' ); my @RECURRENCE_EXCEPTION= ( 'deleted', 'checked-off' ); # ---------------------------------------------------------------------------- sub new { my $class= shift; my $factor= shift; my $obj= { 'recurrence' => $factor, 'recurrence_text' => &get_bit_text ($factor, \@RECURRENCE_TEXT), 'cycle' => shift, 'rec_days' => shift, 'rec_months' => shift, 'duration_begin' => shift, 'duration_end' => shift, }; bless $obj; } # ---------------------------------------------------------------------------- # decode the recurrence status of an ADB record # for details about the data structure, see adb-format.html sub decode { my $class= shift; my $factor= shift; my $b= shift; my $error= 0; my $lng= length ($b); my ($cycle, $rec_days, $rec_months)= unpack ('Cvv', substr ($b, 0, 5)); my $rep_beg= &HP200LX::DB::fmt_date (substr ($b, 5, 3)); my $rep_end= &HP200LX::DB::fmt_date (substr ($b, 8, 3)); my $obj= { 'recurrence' => $factor, 'recurrence_text' => &get_bit_text ($factor, \@RECURRENCE_TEXT), 'cycle' => $cycle, 'rec_days' => $rec_days, 'rec_months' => $rec_months, 'duration_begin' => $rep_beg, 'duration_end' => $rep_end, }; my ($off, $cnt); if ($lng == 18) { # NOTE: there does not seem to be any other indication of the # data type here except the total length $obj->{type}= 'checked-off'; my ($idx, $prev, $next, $main)= unpack ('Cvvv', substr ($b, 0x0B)); $obj->{check_off_pointer}= { 'idx' => $idx, 'prev' => $prev, 'next' => $next, 'main' => $main, }; } else { $obj->{type}= 'exceptions'; $obj->{exceptions}= []; $cnt= unpack ('C', substr ($b, 0x0B, 1)); print "hide cnt=$cnt, lng=$lng\n"; for ($off= 0x0C; $cnt > 0; $cnt--) { if ($off > $lng) { $error++; last; } my $d= &fmt_date (substr ($b, $off, 3)); my $c= unpack ('C', substr ($b, $off+3, 1)); push (@{$obj->{exceptions}}, { 'date' => $d, 'status' => $c }); $off += 4; } $error++ if ($off < $lng); } if ($error) { print "\n", '-'x72, "\nerror processing recurrence record!\n"; print "lng=$lng cnt=$cnt off=$off\n"; &hex_dump ($b, *STDOUT); &print_recurrence_status ($obj, *STDOUT); } bless $obj; } # ---------------------------------------------------------------------------- # pack the recurrence status of an ADB record sub pack { my $rec= shift; my $b; $b= pack ('Cvv', $rec->{cycle}, $rec->{rec_days}, $rec->{rec_months}); $b .= &pack_date ($rec->{duration_begin}); $b .= &pack_date ($rec->{duration_end}); if ($rec->{type} eq 'checked-off') { my ($idx, $prev, $next, $main)= my $op= $rec->{check_off_pointer}; $b .= pack ('Cvvv', $op->{'idx'}, $op->{'prev'}, $op->{'next'}, $op->{'main'}); } else { my $oe= $rec->{exceptions}; my $cnt= $#$oe; if ($cnt > 254) { print "warning: can't pack $cnt exceptions, truncating to 254!\n"; $cnt= 254; } $b .= pack ('C', $cnt+1); my ($ox); foreach $ox (@$oe) { $b .= &pack_date ($ox->{'date'}); $b .= pack ('C', $ox->{'status'}); } } $b; } # ---------------------------------------------------------------------------- # check-off a recurrence entry sub check_off { my $obj= shift; if ($obj->{type} eq 'exceptions') { print "warning: overwriting recurrence exceptions!\n"; } $obj->{type}= 'checked-off'; my ($idx, $prev, $next, $main)= unpack ('Cvvv', substr ($b, 0x0B)); $obj->{check_off_pointer}= { 'idx' => shift, # index within main entry 'prev' => shift || $no_val, 'next' => shift || $no_val, 'main' => shift || $no_val, }; } # ---------------------------------------------------------------------------- # set recurrence exception # $recurrence->exception (date => status, ...); sub exception { my $obj= shift; my %dates= @_; if ($obj->{type} eq 'checked-off') { print "warning: overwriting recurrence check-off marker!\n"; } unless ($obj->{type} eq 'exceptions') { $obj->{type}= 'exceptions'; $obj->{exceptions}= []; } my $ex= $obj->{exceptions}; my ($d); foreach $d (sort keys %dates) { push (@$ex, { 'date' => $d, 'status' => $dates{$d}}); } } # ---------------------------------------------------------------------------- sub get_bit_text { my $val= shift; my $text= shift; my ($str, $i); # $str= "$val:"; for ($i= 0; $i <= $#$text; $i++) { if ($val & $BITS[$i]) { $str .= ' ' if ($str); $str .= $text->[$i]; } } $str; } # ---------------------------------------------------------------------------- sub get_recurrence_wdays_text { &get_bit_text (shift, \@RECURRENCE_WDAY); } # ---------------------------------------------------------------------------- sub get_recurrence_months_text { &get_bit_text (shift, \@RECURRENCE_MONTH); } # ---------------------------------------------------------------------------- sub get_recurrence_days_text { my $val= shift; my $str; # $str .= sprintf (" [rec_days=0x%04X]", $val); if ($val & 0x0080) { $str .= &get_bit_text ($val >> 8, \@RECURRENCE_DAY); $str .= ' '. &get_recurrence_wdays_text ($val & 0x7F); } else { $str= sprintf (" on the %d.", $val & 0x7F); } $str; } # ---------------------------------------------------------------------------- sub print_recurrence_status { my $obj= shift; local *FO= shift; my $recurrence= $obj->{recurrence}; my $str= $obj->{recurrence_text}; $str .= ', cycle='. $obj->{cycle} if ($recurrence >= 2 && $recurrence <= 16); if ($recurrence == 4) { $str .= ', '. &get_recurrence_wdays_text ($obj->{rec_days} & 0x7F); } if ($recurrence >= 8) { $str .= ', '. &get_recurrence_days_text ($obj->{rec_days}); } if ($recurrence >= 16) { $str .= ' of '. &get_recurrence_months_text ($obj->{rec_months}); # $str .= sprintf (" [rec_months=0x%04X]", $obj->{rec_months}); } print FO <{duration_begin}..$obj->{duration_end} EOX if ($obj->{type} eq 'exceptions') { my $ex= $obj->{exceptions}; if ($#$ex >= 0) { print FO "exceptions:\n", my $inst; foreach $inst (@$ex) { printf FO (" %s %02X %s\n", $inst->{'date'}, $inst->{'status'}, @RECURRENCE_EXCEPTION [$inst->{'status'}] || '??'); } } } elsif ($obj->{type} eq 'checked-off') { my $ptr= $obj->{check_off_pointer}; print FO "checked-off item:\n", " main entry/idx: $ptr->{main}/$ptr->{idx}\n", " prev: $ptr->{prev}\n", " next: $ptr->{next}\n"; } else { print FO "unknown recurrence status: ", $obj->{type}, "\n"; } } # ---------------------------------------------------------------------------- sub export_to_vCalendar { my $obj= shift; my $time= shift || 'T00:00:00'; my $rule= $RECURRENCE_XAPIA{$obj->{recurrence}} . $obj->{cycle} . ' ' . $obj->{duration_end} . $time; my $exdate= join (',', map { $_->{'date'} . $time } @{$obj->{exceptions}}); my $res= { 'RRULE' => $rule, 'DTSTART' => $obj->{duration_begin} . $time, }; $res->{'EXDATE'}= $exdate if ($exdate); $res; } # ---------------------------------------------------------------------------- 1;