eval 'exec perl -x $0 ${1+"$@"}' # -*-perl-*-
  if 0;
#!perl -w
#
# ======================================================================
# This file is Copyright 1998,1999 by the Purdue Research Foundation and
# may only be used under license.  For terms of the license, see the
# file named COPYRIGHT included with this software release.
# AAFID is a trademark of the Purdue Research Foundation.
# All rights reserved.
# ======================================================================
#
# Util::FiniteQueue package
#
# AAFID project, COAST Laboratory, CERIAS, 1998-1999.
#
# Diego Zamboni, Jan 27, 1998.
#
# $Id: FiniteQueue.pm,v 1.5 1999/09/08 04:37:58 zamboni Exp $
#
# NOTE: This file is in Perl's POD format. For more information, see the 
#       manual page for perlpod(1).
#

=head1 NAME

Util::FiniteQueue --- Keep values for a certain period of time.

=cut

package Util::FiniteQueue;

use strict;

=head1 SYNOPSIS

  $t=Util::FiniteQueue->new;
  $t->timelength(10);  # in seconds
  for ($i=0; $i<=10; $i++) {
    print "Current queue:   ".$t->toString."\n";
    sleep(3);
    $t->add($i+1);
  }
  $t->reset;
  $t->maxlength(10);  # number of elements
  for ($i=0; $i<=20; $i++) {
    print "Current queue:   ".$t->toString."\n";
    sleep(1);
    $t->add($i+1);
  }
  $t->clear;
  $t->timelength(10);  # now both maxlength and timelength are set
  for ($i=0; $i<=20; $i++) {
    print "Current queue:  ". $t->toStringDetailed ."\n";
    sleep(rand(4));
    $t->add($i+1);
  }

=head1 DESCRIPTION

An object of this class keeps values given using the C<add> method. The
values are stored in chronological order, and are pruned from the queue
when they have been there for more than the time specified by C<timelength>,
or when the queue grows longer than the length given by C<maxlength>.

=head1 METHODS

=over 4

=item new()

Creates a new empty Util::FiniteQueue object.

=cut

# Create an empty queue.
sub new {
  my $class=shift;
  # Can also be called as an instance method.
  $class=ref($class) if ref($class);
  my $self={};
  bless $self, $class;
  # Initialize the queue data structure
  $self->{Queue}=[];
  $self->{QueueLen}=0;
  return $self;
}

=item timelength( [TIME] )

Specifies the maximum time, in seconds, that each value will be kept
in the queue. Older elements are pruned whenever one of its access
methods (C<add>, C<avg>, C<sum>, etc.) is called.

If no argument is given, C<timelength> simply returns the currently
set time length (this is returned even if an argument is given).

If neither C<timelength> nor C<maxlength> are specified, the queue is
never pruned. If both are specified at the same time, elements are
removed whenever they exceed any of the two limits.

The queue is not automatically updated after calling C<timelength>.

=cut

# Set limits
sub timelength {
  my $self=shift;
  my $limit=shift;
  if ($limit) {
    $self->{TimeLength}=$limit;
  }
  return $limit;
}

=item maxlength( [SIZE] )

Specifies the maximum length that the queue will be allowed to have
before the oldest elements are pruned. The queue is updated (extra
elements pruned) only when one of its access methods (C<add>, C<avg>,
C<sum>, etc.) is called.

If no argument is given, C<maxlength> returns the currently set length
(this is returned even if an argument is given).

If neither C<timelength> nor C<maxlength> are specified, the queue is
never pruned. If both are specified at the same time, elements are
removed whenever they exceed any of the two limits.

The queue is not automatically updated after calling C<maxlength>

=cut

sub maxlength {
  my $self=shift;
  my $limit=shift;
  if ($limit) {
    $self->{MaxLength}=$limit;
  }
  return $limit;
}

=item add( [VALUE, ...] )

Adds the given values to the list and update it.

=cut

# Add an element.
sub add {
  my $self=shift;
  # Each element is stored in the queue as a hash ref of the form
  # { Value => value, Time => time}, where value is the element
  # itself and time is the time at which it was added.
  push @{$self->{Queue}}, map { {Value => $_, Time => time() } } @_;
  $self->update;
}

=item visit( SUBROUTINE )

After updating the queue, calls the subroutine given once for each
value in the queue, in the order they appear in the queue (oldest to
youngest). Each time, the subroutine is passed the queue itself, the
current value and its position in the queue as arguments. Returns a list
containing the value returned by the subroutine for each element.

The list is not updated between calls to the subroutine, only before
the first call to it.

=cut

# Process each element with a subroutine.
sub visit {
  my $self=shift;
  my $sub=shift;
  my $i;
  my @res;
  $self->update;
  for ($i=0; $i<$self->length; $i++) {
    push @res, &{$sub}($self, $self->element($i), $i);
  }
  return @res;
}

=item processWith( SUBROUTINE )

After updating the queue, calls the subroutine given one time for all
the queue. It is passed as arguments the queue itself and all the
elements in the list. Returns the value returned by the subroutine.

=cut

# Process all elements at once with a subroutine.
sub processWith {
  my $self=shift;
  my $sub=shift;
  $self->update;
  return &{$sub}($self, $self->elements);
}

=item toString

Returns a string containing all the elements, separated by spaces, after
updating the queue.

=cut

sub toString {
  my $self=shift;
  # The internal method _elemToString is used to obtain the string
  # value of each element, so subclasses of FiniteQueue can override
  # it according to the value they store.
  $self->update;
  return join " ", $self->visit(\&_elemToString);
}

=item toStringDetailed

Returns a string containing all the elements, indicating the time that
each one has been in the queue, after updating it.

=cut

sub toStringDetailed {
  my $self=shift;
  $self->update;
  return join " ", $self->visit(\&_elemToStringDetailed);
}

=item length

Returns the current queue length. The queue is not updated.

=cut

# Return the list length
sub length {
  my $self=shift;
  return $self->{QueueLen};
}

=item elements

Returns a list containing all the elements, after updating the queue.

=cut

# Return the elements of the queue in a list.
sub elements {
  my $self=shift;
  $self->update;
  return map { $_->{Value} } @{$self->{Queue}};
}

=item elements_times

After updating the queue, returns a list containing all the elements,
together with the time at which each one was added to the queue. The
list returned is of the form

  ({Value => elem, Time => time}, ...)

This is, it is a list of hash references, where each hash reference
contains two elements indexed by "Value", the element itself, and
"Time", the time at which it was added.

=cut

sub elements_times {
  my $self=shift;
  $self->update;
  return @{$self->{Queue}};
}

=item element(n)

Returns element in position n of the queue. The first element has index
zero. The queue is not updated.

=cut

# Return the value of a single element
sub element {
  my $self=shift;
  my $pos=shift;
  return $self->{Queue}[$pos]->{Value};
}

=item elemtime(n)

Returns the time at which the element in position n of the queue was
added. The queue is not updated.

=cut

# Return the time at which an element was added.
sub elemtime {
  my $self=shift;
  my $pos=shift;
  return $self->{Queue}[$pos]->{Time};
}

=item update

Forces an update (removal of old or extra elements). Returns the object
itself.

=cut

# Update according to both time and length limits.
sub update {
  my $self=shift;
  my $timelimit=$self->{TimeLength};
  my $lengthlimit=$self->{MaxLength};
  # Do nothing if no limits were specified.
  return if (!$timelimit && !$lengthlimit);
  if ($timelimit) {
    # Prune by time
    $self->{Queue}=[grep {(time()-$_->{Time})<=$timelimit} @{$self->{Queue}}];
  }
  if ($lengthlimit) {
    # Prune by length
    my $ind=$self->{QueueLen}-$lengthlimit;
    $ind=0 if $ind<0;
    $self->{Queue}=[splice(@{$self->{Queue}}, $ind)];
  }
  $self->{QueueLen}=scalar(@{$self->{Queue}});
  return $self;
}

=item clear

Removes all the elements from the list. Returns the object.

=cut

sub clear {
  my $self=shift;
  $self->{Queue}=[];
  $self->{QueueLen}=0;
  return $self;
}

=item reset

Same as clear, plus clears both C<timelength> and C<maxlength>. Returns
the object.

=cut

sub reset {
  my $self=shift;
  $self->clear;
  delete $self->{TimeLength};
  delete $self->{MaxLength};
  return $self;
}

=item isEmpty

Returns true if the queue has no elements.

=cut

sub isEmpty {
  my $self=shift;
  return ($self->length()==0);
}

=back 4

=head1 AUTHOR

Diego Zamboni (zamboni@cs.purdue.edu)

=cut

#######
#
# Internal methods

# Return the string value of an element.
# By default, return the element value itself.
sub _elemToString {
  my $self=shift;
  my $elem=shift;
  my $pos=shift;
  return "$elem";
}

# Return the string value of an element together with its time in the queue.
sub _elemToStringDetailed {
  my $self=shift;
  my $elem=shift;
  my $pos=shift;
  my $curtime=time();
  return "$elem (".($curtime-$self->elemtime($pos)).")";
}

1;
