The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
###########################################
package Cache::Historical;
###########################################
use strict;
use warnings;
use Rose::DB::Object::Loader;
use File::Basename;
use File::Path;
use Log::Log4perl qw(:easy);
use DBI;
use DateTime::Format::Strptime;

our $VERSION = "0.05";

###########################################
sub new {
###########################################
    my($class, %options) = @_;

    my($home)     = glob "~";
    my $default_cache_dir = "$home/.cache-historical";

    my $self = {
        sqlite_file => "$default_cache_dir/cache-historical.dat",
        %options,
    };

    my $cache_dir = dirname( $self->{sqlite_file} );

    if(! -d $cache_dir ) {
        mkpath [ $cache_dir ] or
            die "Cannot mktree $cache_dir ($!)";
    }

    bless $self, $class;

    $self->{dsn} = "dbi:SQLite:dbname=$self->{sqlite_file}";

    if(! -f $self->{sqlite_file}) {
        $self->db_init();
    }

    my $loader =
    Rose::DB::Object::Loader->new(
        db_dsn        => $self->{dsn},
        db_options    => { AutoCommit => 1, RaiseError => 1 },
        class_prefix  => 'Cache::Historical',
        with_managers => 1,
    );

    $loader->make_classes();

    $self->{loader} = $loader;

    return $self;
}

###########################################
sub make_modules {
###########################################
    my($self, @options) = @_;

    DEBUG "Making modules in @options";
    $self->{loader}->make_modules( @options );
}

###########################################
sub dbh {
###########################################
    my($self) = @_;

    if(! $self->{dbh} ) {
        $self->{dbh} = DBI->connect($self->{dsn}, "", "");
    }

    return $self->{dbh};
}

###########################################
sub db_init {
###########################################
    my($self) = @_;

    my $dbh = $self->dbh();

    DEBUG "Creating new SQLite db $self->{sqlite_file}";

    $dbh->do(<<'EOT');
CREATE TABLE vals (
  id       INTEGER PRIMARY KEY,
  date     DATETIME,
  upd_time DATETIME,
  key      TEXT,
  value    TEXT,
  UNIQUE(date, key)
);
EOT

    $dbh->do(<<'EOT');
CREATE INDEX vals_date_idx ON vals(date);
EOT

    $dbh->do(<<'EOT');
CREATE INDEX vals_key_idx ON vals(key);
EOT

    return 1;
}

###########################################
sub set {
###########################################
    my($self, $dt, $key, $value) = @_;

    DEBUG "Setting $dt $key => $value";

    my $r = Cache::Historical::Val->new();
    $r->key( $key );
    $r->date( $dt );
    $r->upd_time( DateTime->now() );
    $r->load( speculative => 1 );
    $r->value( $value );
    $r->save();
}

###########################################
sub get {
###########################################
    my($self, $dt, $key, $interpolate) = @_;

    my @date_query = (date => $dt);
    @date_query = (date => {le => $dt}) if $interpolate;

    my $values = Cache::Historical::Val::Manager->get_vals(
        query => [
          @date_query,
          key   => $key,
        ],
        sort_by  => "date DESC",
        limit => 1,
    );

    if(@$values) {
        my $value = $values->[0]->value();
        DEBUG "Getting $dt $key => $value";
        return $value;
    }

    return undef;
}

###########################################
sub keys {
###########################################
    my($self) = @_;

    my @keys;
    my $keys = Cache::Historical::Val::Manager->get_vals(
        distinct => 1,
        select   => [ 'key' ],
    );

    for(@$keys) {
        push @keys, $_->key();
    }

    return @keys;
}

###########################################
sub values {
###########################################
    my($self, $key) = @_;

    my @values = ();
    my @key = ();
    @key = (key => $key) if defined $key;

    my $values = Cache::Historical::Val::Manager->get_vals(
        query => [ @key ],
        sort_by => ['date'],
    );

    for(@$values) {
        push @values, [$_->date(), $_->value()];
    }

    return @values;
}

###########################################
sub last_update {
###########################################
    my($self, $key) = @_;

    my @key = ();
    @key = (key => $key) if defined $key;

    my $values = Cache::Historical::Val::Manager->get_vals(
        query => [ @key ],
        sort_by => ['upd_time DESC'],
        limit   => 1,
    );

    if(@$values) {
        my $date = $values->[0]->upd_time();
        return $date;
    }

    return undef;
}

###########################################
sub since_last_update {
###########################################
    my($self, $key) = @_;

    my $date = $self->last_update($key);

    if(defined $date) {
        return DateTime->now() - $date;
    }

    return undef;
}

###########################################
sub get_interpolated {
###########################################
    my($self, $dtp, $key) = @_;

    return $self->get($dtp, $key, 1);
}

my $date_fmt = DateTime::Format::Strptime->new(
                  pattern => "%Y-%m-%d %H:%M:%S");

###########################################
sub time_range {
###########################################
    my($self, $key) = @_;

    my $dbh = $self->dbh();

    my($from, $to) = $dbh->selectrow_array(
       "SELECT MIN(date), MAX(date) FROM vals WHERE key = " .
       $dbh->quote( $key ));

    $from = $date_fmt->parse_datetime( $from );
    $to   = $date_fmt->parse_datetime( $to );

    return($from, $to);
}

###########################################
sub clear {
###########################################
    my($self, $key) = @_;

    my @params = (all => 1);

    if(defined $key) {
        @params = ("where" => [ key => $key ]);
    }

    my $values = Cache::Historical::Val::Manager->delete_vals( @params );
}

1;

__END__

=head1 NAME

Cache::Historical - Cache historical values

=head1 SYNOPSIS

    use Cache::Historical;

    my $cache = Cache::Historical->new();
 
       # Set a key's value on a specific date
    $cache->set( $dt, $key, $value );

       # Get a key's value on a specific date
    my $value = $cache->get( $dt, $key ); 

       # Same as 'get', but if we don't have a value at $dt, but we 
       # do have values for dates < $dt, return the previous 
       # historic value. 
    $cache->get_interpolated( $dt, $key );

=head1 DESCRIPTION

Cache::Historical caches historical values by key and date. If you have
something like historical stock quotes, for example

    2008-01-02 msft 35.22
    2008-01-03 msft 35.37
    2008-01-04 msft 34.38
    2008-01-07 msft 34.61

then you can store them in Cache::Historical like

    my $cache = Cache::Historical->new();

    my $fmt = DateTime::Format::Strptime->new(
                  pattern => "%Y-%m-%d");

    $cache->set( $fmt->parse_datetime("2008-01-02"), "msft", 35.22 );
    $cache->set( $fmt->parse_datetime("2008-01-03"), "msft", 35.37 );
    $cache->set( $fmt->parse_datetime("2008-01-04"), "msft", 34.38 );
    $cache->set( $fmt->parse_datetime("2008-01-07"), "msft", 34.61 );

and retrieve them later by date:

    my $dt = $fmt->parse_datetime("2008-01-03");

      # Returns 35.37
    my $value = $cache->get( $dt, "msft" );

Even if there's no value available for a given date, but there are historical 
values that predate the requested date, C<get_interpolated()> will return
the next best historical value:

    my $dt = $fmt->parse_datetime("2008-01-06");

      # Returns undef, no value available for 2008-01-06
    my $value = $cache->get( $dt, "msft" );

      # Returns 34.48, the value for 2008-01-04, instead.
    $value = $cache->get_interpolated( $dt, "msft" );

=head2 Methods

=over 4

=item new()

Creates the object. Takes the SQLite file to put the date into as
an additional parameter:

    my $cache = Cache::Historical->new(
        sqlite_file => "/tmp/mydata.dat",
    );

The SQLite file defaults to 

    $HOME/.cache-historical/cache-historical.dat

so if you have multiple caches, you need to use different
SQLite files.

=item time_range()

       # List the time range for which we have values for $key
    my($from, $to) = $cache->time_range( $key );

=item keys()

       # List all keys
    my @keys = $cache->keys();

=item values()

       # List all the values we have for $key, sorted by date
       # ([$dt, $value], [$dt, $value], ...)
    my @results = $cache->values( $key );

=item clear()

       # Remove all values for a specific key
    $cache->clear( $key );

       # Clear the entire cache
    $cache->clear();

=item last_update()

       # Return a DateTime object of the last update of a given key
    my $when = $cache->last_update( $key );

=item since_last_update()

       # Return a DateTime::Duration object since the time of the last
       # update of a given key.
    my $since = $cache->since_last_update( $key );

=back

=head1 LEGALESE

Copyright 2007-2011 by Mike Schilli, all rights reserved.
This program is free software, you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 AUTHOR

2007, Mike Schilli <cpan@perlmeister.com>