rolling forward inactive tasks

Rick Bradley <[email protected]> Fri, 12 Sep 2003 09:53:02 -0500
Newsgroups gmane.comp.gnome.apps.mr-project.user
Message-ID <[email protected]>
Where I use mrproject we have just a couple of people we can assign as
resources to various tasks, but we have a lot of tasks.  We entered task
information into mrproject along with task dependencies and now have a
nice Gantt chart to play with.  Our desire is to use mrproject to manage
dependencies and to do schedule estimation even for unstarted tasks.

I'm aware that the way we're using mrproject is probably different from
the way a number of people are using it:  we don't mind if early
non-critical events slip until we get around to them; and we're tracking
a number of smaller "projects" inside a single mrproject project file.

Since we can only work on a few things at a time only a couple of tasks
a day will have any progress associated with them.  There are then a lot
of tasks with no predecessors that are available to start "as soon as
possible" which stay stuck at the same place on the Gantt chart and
eventually end up entirely in the past even though noone has gotten
around to working on them.

We could constrain them to be "start-no-earlier-than" on a given date,
but eventually that date will pass and we'd have to update them to keep
them from slipping into the past as well.

So I went ahead and added a start-no-earlier-than constraint to the
tasks with no predecessors and wrote a Perl script that updates the
mrproject XML project file by incrementing the constraint date and
start/end dates for tasks with no recorded progress which have a
start-no-earlier-than constraint whose time now lies in the past.  We
run this once a day now, though running it multiple times a day won't
corrupt anything, and running it after an indefinite hiatus works as
well.

The script appears below.

Rick
-- 
 http://www.rickbradley.com    MUPRN: 915
                       |  pack my computer.
   random email haiku  |  I'm moving from Greensborough,
                       |  up to Buffalo.

#!/usr/bin/perl -w

#
# reschedule-projects.pl:
#
#   Update a 'mrproject' project file by pushing forward all tasks
#   with no recorded progress and a 'start-no-earlier-than' constraint
#   prior to the current date.
#
# Copyright (c) 2003, Base Systems (http://www.basesys.com/)
# and  Rick Bradley ([email protected] / [email protected])
#
# This software is made available under the terms of the author's
# version of the BSD license, which may be found online at:
#
#     <http://www.rickbradley.com/misc/bsd-license.html>
#

use Date::Parse;
use XML::Simple;
use File::Temp;
use File::Copy;
use Data::Dumper;

use strict;
use vars qw($xml $tree @list @tasks $task @parts $today $text %good $start $end
            $fh $file $looking);

$xml = new XML::Simple('forcearray' => 0, 'keyattr' => [], 'forcecontent' => 0, 'xmldecl' => 1, 'keeproot' => 1);

# read in mrproject data from XML file
$tree = $xml->XMLin($ARGV[0]);

# print Dumper($tree);

# flatten list of nested tasks
@list =  @{$tree->{'project'}->{'tasks'}->{'task'}};
while (@list) {
    $task = pop(@list);
    push(@tasks, $task);
    if ($task->{'task'}) {
        if ('ARRAY' eq ref($task->{'task'})) {
            push (@list, @{$task->{'task'}}) 
        } else {
            push (@list, $task->{'task'}) 
        }
    }
}

# get current date for comparison with constraint date format
@parts = localtime(time());
$today = sprintf("%4d%02d%02d", 1900+$parts[5], 1+$parts[4], $parts[3]);

# identify 0-indegree tasks with no progress and a floatable start constraint
map { $good{$_->{'id'} + 0} = 1;
    print "found [$_->{id}]::[$_->{name}]\n"; }
    grep { $today gt $_->{'constraint'}->{'time'} }
        grep { exists $_->{'constraint'} and 'start-no-earlier-than' eq $_->{'constraint'}->{'type'} }
            grep { '0' eq $_->{'percent-complete'} }
                grep { not exists $_->{'predecessors'} }
                    @tasks;


# now update XML file with new dates
open (IN, "< $ARGV[0]") or 
    die "$0: Cannot read XML file [$ARGV[0]]: $!\n";
($fh, $file) = mkstemp( "mrprojXXXXX");

die "$0: could not create temp file: $!\n" unless $file;

$looking = 0;
while (<IN>) {
    # process constraints
    if ($looking) {
        # already seen a task, fix its constraint
        if (/^\s*<constraint/) {
            # update existing constraint
            s/time="[^"]+"/time="$looking"/;
            print $fh $_;
            $looking = 0;
            next;
        }
        $looking = 0;
    }

    # looking for a task
    if (/^\s*<task\s+.*id="([^"]+)"/ and $good{$1 + 0}) {
        # found a task of interest
        
        # extract start, end timestamps
        ($start, $end) = /(?=.*start="([^"]+)")(?=.*end="([^"]+))/;

        # push dates forward
        $start = bump_date($start);
        $end = bump_date($end);

        # update task line
        s/start="[^"]+"/start="$start"/;
        s/end="[^"]+"/end="$end"/;

        # and force constraint update
        $looking = $start;
    }
    print $fh $_;
}
close(IN);
close($fh);

move($file, $ARGV[0]) or
    die "$0: could not move [$file] to [$ARGV[0]]: $!\n";

sub bump_date {
    my $date = shift;
    my ($front, $parsed, @parts);

    # extract date 
    $front = substr($date, 0, 8);
    $parsed = str2time($front) or 
        die "$0: could not parse date [$date]\n";
 
    # increment day
    $parsed += 60*60*24;

    # rebuild date
    @parts = localtime($parsed);
    substr($date, 0, 8) = sprintf("%4d%02d%02d", 1900+$parts[5], 1+$parts[4], $parts[3]);

    $date;
}