Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

Extensibility

The TimeSeries Concept
Defining New TimeSeries algorithms
Defining a New TimeSeries Type

This section describes how to extend the Boost.Time_series library by adding new algorithms that work with the series types, and how to define new series types that satisfy the TimeSeries concept.

All of the series types in the Time_series library, whether they are dense or sparse or constant or even an adapted view of any of those, share a common interface and can be processed generically. This is because they all model the same concept: TimeSeries. The TimeSeries concept itself is a refinement of a lower-level interface, called InfiniteRangeRunStorage. Understanding how InfiniteRangeRunStorage can be used to represent and manipulate all these different types of series is key to being able to extend the Time_series library.

The Range-Run Abstraction

The range-run abstraction is the core of the Time_series library. Just as iterators in the STL make it possible to traverse the elements of an STL contaier without knowing the details of how it is layed out in memory, so too does the range-run abstraction.

The easiest way to visualize range-run storage is as an array of (value, offset, end_offset) tuples. Each tuple in the array represents a run of elements with the same value, stretching from the offset to the end-offset. The run is half-open -- the offset is inclusive, but the end-offset is exclusive -- just as STL iterator ranges are, but offsets are position indicators, not iterators.

The Range_run_storage library provides utilities and a handful of algorithms for stepping through and manipulating the runs in a series.

The following table shows all the valid expressions on models of the TimeSeries concept. In the table, S is a (possibly const-qualified) type which models TimeSeries, s is an object of type S, and o is an object of S's offset_type. All types and functions are assumed to be in the ::boost namespace, except the TimeSeries<> template, which is in the ::boost::time_series::concepts namespace.

Table 1.3. TimeSeries Valid Expressions

Expression Type Semantics
sequence::begin( s ) TimeSeries< S >::cursor A cursor that can be used to traverse the elements and runs of s.
sequence::end( s ) TimeSeries< S >::end_cursor A cursor that can be used to mark the end of the traversal of the elements and runs of s.(Note: for all series types in the Time_series library, end_cursor is the same type as cursor.)
sequence::elements( s ) TimeSeries< S >::elements A ReadablePropertyMap that, when accessed with a cursor, returns the element at the specified position.
range_run_storage::runs( s ) TimeSeries< S >::runs A ReadablePropertyMap that, when accessed with a cursor, returns the Run at the specified position.
range_run_storage::get_at( s, o ) TimeSeries< S >::value_type Returns the element at the offset o. This is generally an O(log N) operation, except for dense series, where it is O(1).
range_run_storage::zero( s ) TimeSeries< S >::zero_type Returns the value of the zero elements of the series. zero_type must be convertible to value_type.
range_run_storage::pre_run( s ) TimeSeries< S >::pre_run_type A Run that comes before the runs in the runs property map. Unlike the runs in the runs property map, this run may be infinite or empty.
range_run_storage::pre_value( s ) TimeSeries< S >::value_type The value associated with the pre_run, if there is one.
range_run_storage::post_run( s ) TimeSeries< S >::post_run_type A Run that comes after the runs in the runs property map. Unlike the runs in the runs property map, this run may be infinite or empty.
range_run_storage::post_value( s ) TimeSeries< S >::value_type The value associated with the post_run, if there is one.
s.discretization() TimeSeries< S >::discretization_type A value which represents the series' discretization.
s[o] TimeSeries< S >::value_type Same as range_run_storage::get_at( s, o ).

Every series type in the Boost.Time_series library exposes all of its data via these functions. In addition, most of the series types provide mechanisms for mutating the data, or building an entirely new series. These series are models of the Mutable_TimeSeries concept, which has the following valid expressions in addition to those of the TimeSeries concept. In this table, S is a (possibly const-qualified) type which models Mutable_TimeSeries, s is an object of type S, r is an object of S's run_type, and v is an object of S's value_type.

Table 1.4. Mutable_TimeSeries Valid Expressions

Expression Type Semantics
range_run_storage::set_at( s, r, v ) void Sets a run r within s to value v. This operation is O(N) in the worst case, and invalidates any cursors to s.
range_run_storage::zero( s, v ) void Sets the value of the zero elements of the series.
range_run_storage::pre_value( s, v ) void Sets a the value of the pre-run within s to v.
range_run_storage::post_value( s, v ) void Sets a the value of the post-run within s to v.
range_run_storage::ordered_inserter( s ) Mutable_TimeSeries< S >::ordered_inserter_type Returns an OrderedInserter for s that can be used to build a new series in s.

In addition, the elements property map of Mutable_TimeSeries is a model of ReadWritePropertyMap, which means the elements of the series can be written to. All of the series types besides the views (clipped_series<>, scaled_series<> and shifted_series<>) are models of Mutable_TimeSeries.

The OrderedInserter concept makes efficient initialization of time series possible. You use an OrderedInserter by pushing runs and values into it in order, and when you are done, you call commit() on it. The OrderedInserter concept has the following valid expressions. In the table below, o is a model of an OrderedInserter, r is a run, and v is a value.

Table 1.5. OrderedInserter Valid Expressions

Expression Type Semantics
range_run_storage::set_at( o, r, v ) void Adds a run r with value v. This operation is amortized O(1). The offset of the run r must be greater than or equal to the end-offset of any run added previously.
range_run_storage::commit( o ) void Writes all the runs and values into the underlying series. The complexity is O(1).

The easiest way to implement a new Time_series algorithm is to implement it in terms of the lower-level InfiniteRangeRunStorage algorithms: copy(), for_each(), and transform(). These algorithms take care of the details of stepping through a series one run at a time, or in the case of the 2-series variant of transform(), stepping through two series in tandem.

Using range_run_storage::for_each()

The simplest of these algorithms is for_each(). It accepts an InfiniteRangeRunStorage and a TernaryFunction. For each run in the series, the function is invoked with the run's value, offset and end offset. The following example uses for_each() to print a series to std::cout:

// A TernaryFunction for printing runs to std::cout
struct display_run
{
    template< class Value, class Offset >
    void operator()( Value const & value, Offset start, Offset stop ) const
    {
        std::cout
            << " value = " << value << ", "
            << " offset = " << start << ", "
            << " end offset = " << stop << '\n';
    }
};

// Uses for_each() and display_run to print a series to std::cout
template< class Series >
void print_series( Series const & series )
{
    range_run_storage::for_each( series, display_run() );
}

Many of the TimeSeries algorithms are trivially implementable in terms of for_each(); for example, see adjacent_difference().

Using range_run_storage::copy()

Another option would be to use the copy() algorithm, which is like for_each() except that it accepts an OrderedInserter instead of a TernaryFunction. The OrderedInserter interface is more complicated, but also more powerful. All models of Mutable_TimeSeries expose an OrderedInserter so they can be modified, but anything that satisfies the OrderedInserter interface can be used by copy() -- it doesn't necessarily have to be an inserter into a series. It may calculate a single value, for instance.

The OrderedInserter interface offers more opportunities for optimization than TernaryFunction. When your algorithm can be made more efficient when handling unit-length runs (as with sparse, dense and delta series), you will want to use copy() instead of for_each(), because copy() makes that information available at compile time.

Using range_run_storage::transform()

For more complex tasks, the transform() algorithm is indespensible. There are two basic variants, which are analogous to the two std::transform() overloads in the standard: one that takes one series, and another that takes two.

Here is an example of using transform() to negate every element in a series and write the result into another, using only the low-level InfiniteRangeRunStorage interface:

dense_series< double > from, to;
...
Mutable_InfiniteRangeRunStorage< dense_series< double > >::
    ordered_inserter_type out( range_run_storage::ordered_inserter( to ) );

range_run_storage::transform( from, std::negate< double >(), out );
range_run_storage::commit( out );

And here is an example of using transform() to add two series and write the result into a third:

piecewise_constant_series< double > left, right, to;
...
Mutable_InfiniteRangeRunStorage< dense_series< double > >::
    ordered_inserter_type out( range_run_storage::ordered_inserter( to ) );

range_run_storage::transform( left, right, std::plus< double >(), out );
range_run_storage::commit( out );

This does what you would expect: it performs an element-wise addition of the two series. Note that the runs in one series may or may not overlap with runs in the other. Where runs do overlap, the result will be the addition of the two runs' values. Elsewhere, the result is the value from the one run plus the zero of the other series.

Some complicated algorithms need to do something different depending on whether or not runs overlap. There is a special version of transform() you can use in those situations. This version lets you specify three functions: a binary one to call when runs overlap, and two unary ones to call when a left run doesn't overlap a right and vice versa. These three functions may or may not return the same type. Below is a longer example that shows how you would use this version of transform() to detect in an OrderedInserter the different ways the runs overlap.

struct left_t {};
struct right_t {};
struct both_t {};

left_t  do_left( int ) { return left_t(); }
right_t do_right( int ) { return right_t(); }
both_t  do_both( int,int ) { return both_t(); }

struct stub_ordered_inserter
{
    typedef std::pair< std::ptrdiff_t, std::ptrdiff_t > run_t;
    
    void set_at( run_t, left_t ) {}  // for left runs only
    void set_at( run_t, right_t ) {} // for right runs only
    void set_at( run_t, both_t ) {}  // where left and right overlap
};

...

time_series::piecewise_constant_series< int > left, right;

// Call transform() with different functions for where runs do and do not overlap.
stub_ordered_inserter o;
range_run_storage::transform( left, right, do_both, do_left, do_right, o );

Now imagine that the series left and right contain one run each:

left  = [0, 2) of value 1 
right = [1, 3) of value 2

What is the sequence of operations executed by transform()? It depends on how the runs from left and right overlap. The following table shows the different sub-runs of the two sequence and the operations transform() performs for each:

Left sub-runs Right sub-runs overlap operation
[0,1) of value 1   no overlap do_left(1)
[1,2) of value 1 [1,2) of value 2 overlap do_both(1,2)
  [2,3) of value 2 no overlap do_right(2)

This example demonstrates a number of interesting and useful features of the transform() function and OrderedInserter's in general. First, note that the return types of the three functions do_both(), do_left(), and do_right() can be used to dispatch to different member functions of stub_ordered_inserter. This trick is used in the implementation of many of the Time_series algorithms. Also, note that all we need to do to make a valid OrderedInserter is to define an appropriate set_at() member function. This takes advantage of the default implementations of range_run_storage::set_at() and range_run_storage::commit(), the former calling the set_at() member function and the later being a no-op.

Programming To The RangeRunStorage Interface

Sometimes, the existing InfiniteRangeRunStorage algorithms are not enough to implement a new TimeSeries algorithm, and you need to write your own. In that case, understanding the TimeSeries concept is crucial. When implementing a new algorithm over a TimeSeries (or an InfiniteRangeRunStorage), it can help to start with an existing algorithm and modify it to suit your needs. Here, for example, is how for_each() might be implemented:

namespace seq = boost::sequence;
namespace rrs = boost::range_run_storage;

// Call a ternary function for every (value, offset, end_offset) in 
// an InfiniteRangeRunStorage
//
template< class In, class TernaryOp >
TernaryOp for_each( In &in, TernaryOp fun )
{
    using rrs::concepts::InfiniteRangeRunStorage;

    typedef typename InfiniteRangeRunStorage< In >::cursor cursor_type;
    typedef typename InfiniteRangeRunStorage< In >::runs runs_type;
    typedef typename InfiniteRangeRunStorage< In >::run_type run_type;
    typedef typename InfiniteRangeRunStorage< In >::elements elements_type;
    typedef typename InfiniteRangeRunStorage< In >::post_run_type post_run_type;
    typedef typename InfiniteRangeRunStorage< In >::pre_run_type pre_run_type;

    // Execute "fun" on the pre_run
    pre_run_type pre_run( rrs::pre_run( in ) );
    if(!rrs::empty( pre_run ))
    {
        fun( rrs::pre_value( in ), rrs::offset( pre_run ), rrs::end_offset( pre_run ) );
    }

    runs_type runs = rrs::runs( in );
    elements_type elements = seq::elements( in );
    cursor_type begin = seq::begin( in );
    cursor_type end = seq::end( in );

    // Execute "fun" on the runs in the "runs" property map
    for( ; begin != end; ++begin )
    {
        run_type run = runs( *begin );
        fun( elements( *begin ), rrs::offset( run ), rrs::end_offset( run ) );
    }

    // Execute fun on the post_run
    post_run_type post_run( rrs::post_run( in ) );
    if( !rrs::empty( post_run ) )
    {
        fun( rrs::post_value( in ), rrs::offset( post_run ), rrs::end_offset( post_run ) );
    }

    return fun;
}

Notice how we first process the pre-run, then the runs in the runs property map, and finally the post-run.

Stepping through two series in tandem is a much more difficult problem. Try to express your new algorithm in terms of transform() if at all possible.

Understanding the TimeSeries concept is the first step to implementing a new TimeSeries type. Once you know the basics, the code is unsurprising and rather mechanical. Let's see how it works by implementing a basic TimeSeries that has a fixed size:

template< class Value, size_t Size, class Discretization = int >
struct fixed_series;

The TimeSeries concept is at the top of a refinement heirarchy of concepts. We can begin at the bottom and work our way up the refinement heirarchy until we are done.

Modelling the Sequence Concept

The concept at the bottom of the hierarchy is Sequence, so we will begin there, by making the following fixed_storage<> model Sequence.

template< class Value, size_t Size >
struct fixed_storage
{
    typedef Value value_type;
    enum size_type { size = Size };
    fixed_storage() : elements_() {}
    Value & operator[](ptrdiff_t n) { return elements_[n]; }
    Value const & operator[](ptrdiff_t n) const { return elements_[n]; }
private:
    Value elements_[ Size ];
};

The simplest way to make fixed_storage<> a Sequence is to give it STL iterators and begin() and end() member functions. That would make it a valid Range, which is by definition a valid Sequence. But let's see how to use the Sequence customization points to make this a valid Sequence non-intrusively.

First, we'll need to define some cursors. A cursor is nothing more that a position indicator. It must be incrementable and dereferencable. For position, we can use a plain ptrdiff_t, but we must wrap it in a counting_iterator<> from the Boost.Iterator library to make it dereferencable.

The Sequence customization points use tag dispatching, so let's define a tag type and a specialization of boost::sequence::impl::tag<> as follows:

struct fixed_storage_tag;

namespace boost { namespace sequence { namespace impl
{
    template< class T, size_t N >
    struct tag< fixed_storage< T, N > >
    {
        typedef fixed_storage_tag type;
    };
}}}

Now we can hook the sequence::begin() and sequence::end() functions by specializing their implementation templates in the sequence::impl namespace:

namespace boost { namespace sequence { namespace impl
{
    template< class S >
    struct begin< S, fixed_storage_tag >
    {
        typedef counting_iterator< ptrdiff_t > result_type;
        result_type operator()(S &) const
        {
            return result_type( 0 );
        }
    };

    template< class S >
    struct end< S, fixed_storage_tag >
    {
        typedef counting_iterator< ptrdiff_t > result_type;
        result_type operator()( S & ) const
        {
            return result_type( S::size );
        }
    };
}}}

With these definitions, sequence::begin( s ) will return a counting_iterator< ptrdiff_t >(0) when passed a fixed_storage<>.

[Note] Note

There is no distinction between const cursors and non-const cursors, as there is with iterators. In the Sequence concept, cursors are purely position indicators, and the const-ness is best handled by the elements property map, which we implement next.

To complete the Sequence concept, we need to implement the sequence::elements() customization point. For that, we need a ReadablePropertyMap that converts a ptrdiff_t to an element in the fixed_storage<>. That's fairly straightforward; the only trick is getting the const-ness of the return type correct.

template< class S >
struct indexable_elements
{
    typedef typename S::value_type value_type;
    typedef
        typename mpl::if_< is_const< S >, value_type const &, value_type & >::type
    result_type;
    
    indexable_elements( S & s ) : s_( s ) {}
    
    result_type operator()( ptrdiff_t n ) const
    {
        return s_[ n ];
    }
private:
    S & s_;
};

Now we can use the indexable_elements<> template to implement the sequence::elements() customization point:

namespace boost { namespace sequence { namespace impl
{
    template< class S >
    struct elements< S, fixed_storage_tag >
    {
        typedef indexable_elements< S > result_type;
        
        result_type operator()( S & s ) const
        {
            return result_type( s );
        }
    };
}}}

And that's it. The fixed_storage<> type is now a model of the Sequence concept.

[Note] Note

Making fixed_storage<> model the Mutable_Sequence concept would involve a small change to indexable_elements<> to make it a ReadWritePropertyMap.

Modelling the RangeRunStorage Concept

To make fixed_storage<> model the RangeRunStorage concept, we need to implement the runs(), zero(), and get_at() customization points. Let's look at runs() first. It must return a ReadablePropertyMap that, when used together with a cursor, returns the associated run. Recall that our cursor is merely counting_iterator< ptrdiff_t >, and that for our fixed_storage<>, all the runs will have a length of 1. We can use the unit_run<> template provided by the Range_run_storage library to trivially implement our runs property map:

struct fixed_runs
{
    typedef range_run_storage::unit_run< ptrdiff_t > result_type;
    
    result_type operator()( ptrdiff_t n ) const
    {
        return result_type( n );
    }
};

Then we specialize the runs<> template in the range_run_storage::impl namespace as follows:

namespace boost { namespace range_run_storage { namespace impl
{
    template< class S >
    struct runs< S, fixed_storage_tag >
    {
        typedef fixed_runs result_type;
    
        result_type operator()( S & ) const
        {
            return result_type();
        }
    };
}}}

There is one optimization we should take care of while we're at it. Our runs property map is dense; that is, the runs are all unit length, and the offsets are monotonically increasing. Many low-level algorithms can use this information to select a more efficient implementation. We can use the is_dense_runs<> trait to enable this optimization for our runs property map, as follows:

namespace boost { namespace range_run_storage { namespace traits
{
    template<>
    struct is_dense_runs< fixed_runs >
      : mpl::true_
    {};
}}}

OK, now we're done with the runs property map. Next is zero(). The zero() function returns the value the series takes outside the defined range. It has a default implementation which returns a const reference to a default constructed object of the series' value type. In our case, it would return a double of value 0.0. We decide that this is an acceptable default for us, and we leave it alone.

Finally, we have get_at(). When implementing get_at(), keep in mind that a RangeRunStorage has a defined value everywhere, it just may be zero. You need to check the index and return something sensible. This affects the return type, since you cannot return a non-const reference to the series' zero.

namespace boost { namespace range_run_storage { namespace impl
{
    template< class S, class I >
    struct get_at< S, I, fixed_storage_tag >
    {
        typedef typename S::value_type const & result_type;
        
        result_type operator()( S & s, I & i ) const
        {
            if( i >= 0 && i < S::size )
                return s[i];
            
            return range_run_storage::zero( s );
        }
    };
}}}

And with that, we're done satisfying the RangeRunStorage concept requirements.

Modelling the InfiniteRangeRunStorage Concept

The InfiniteRangeRunStorage concept refines the RangeRunStorage concept with the addition of four customization points: pre_run(), pre_value(), post_run() and post_value(). These are two extra runs before and after the runs in the runs property map. Unlike the runs in the property map, the pre- and post-runs may be infinite or empty. The default implementations of these customization points return empty runs. That means that any RangeRunStorage is also an InfiniteRangeRunStorage. If our series type cannot have a run from -Inf or to +Inf, we don't have to do anything extra beyond what we have done already.

If we wanted our fixed_storage<> to have pre- and post-runs we could add them by partially specializing the appropriate templates in the range_run_storage::impl namespace, as follows:

namespace boost { namespace range_run_storage { namespace impl
{
    template< class S >
    struct pre_run< S, fixed_storage_tag >
    {
        typedef some-run-type result_type;
        
        result_type operator()( S & s ) const
        {
            return the-pre-run;
        }
    };

    template< class S >
    struct pre_value< S, fixed_storage_tag >
    {
        typedef typename S::value_type result_type;
        
        result_type operator()( S & s ) const
        {
            return the-value-of-the-pre-run;
        }
    };
}}}

And likewise for the post-run.

Making Your Series Mutable

To make your series mutable, you first need to make the series model Mutable_Sequence. That involves extending the elements property map to allow writing to the elements of the fixed_storage<>. Our new indexable_elements<> property map looks like this:

template< class S >
struct indexable_elements
{
    typedef typename S::value_type value_type;
    typedef
        typename mpl::if_< is_const< S >, value_type const &, value_type & >::type
    reference;
    
    template<typename Signature> struct result
    {
        typedef void type;
    };
    
    template<typename This, typename A> struct result< This( A ) >
    {
        typedef reference type; // single-argument invocations return a reference
    };
    
    indexable_elements( S & s ) : s_( s ) {}
    
    reference operator()( ptrdiff_t n ) const
    {
        return s_[ n ];
    }

    void operator()( ptrdiff_t n, value_type const & v ) const
    {
        s_[ n ] = v;
    }
private:
    S & s_;
};

With this change, the fixed_storage<> type models Mutable_Sequence, and its elements can be modified in-place. For instance, the following code would set the first element of a fixed_storage<int, 10> to 42:

fixed_storage<int, 10> fs;
sequence::elements(fs)(*sequence::begin(fs), 42);

To model Mutable_RangeRunStorage, you need to implement the set_at(), zero() and ordered_inserter() customization points. Set_at() is easily implemented, as follows:

namespace boost { namespace range_run_storage { namespace impl
{
    template< class S, class R, class V >
    struct set_at< S, R, V, fixed_storage_tag >
    {
        typedef void result_type;
        
        void operator()( S & s, R & r, V & v ) const
        {
            ptrdiff_t off = rrs::offset( r );
            ptrdiff_t endoff = rrs::end_offset( r );
            BOOST_ASSERT( off >= 0 && endoff < S::size );
            std::fill( &s[0] + off, &s[0] + endoff, v );
        }
    };
}}}

A Mutable_RangeRunStorage lets you modify the zero elements of a series with the range_run_storage::zero( series, value ) syntax. The default implementation of this API merely asserts that the new zero value is the same as the old. If you would like a different implementation, you can hook this API as follows:

namespace boost { namespace range_run_storage { namespace impl
{
    template< class S, class V >
    struct set_zero< S, V, fixed_storage_tag >
    {
        typedef void result_type;
        
        void operator()( S & s, V & v ) const
        {
            the-zero-of-s = v;
        }
    };
}}}

Implementing ordered_inserter() is a bit trickier, and involves defining and inserter type that satisfies the OrderedInserter concept. Inserters receive runs in order, and changes are committed at the end. Your inserter should make no changes to the underlying storage until the commit occurs; in particular, the inserter should not invalidate any of the series' cursors until the commit is called.

template< class S >
struct fixed_storage_inserter
{
    fixed_storage_inserter( S & s )
      : old_( s ), new_( new S ), offset_( 0 ) {}
    
    template< class R, class V >
    void set_at( R & r, V & v )
    {
        ptrdiff_t off = rrs::offset( r );
        ptrdiff_t endoff = rrs::end_offset( r );
        BOOST_ASSERT( off >= offset_ && endoff < S::size );
        std::fill( &(*new_)[0] + offset_, &(*new_)[0] + off, rrs::zero( old_ ) );
        std::fill( &(*new_)[0] + off, &(*new_)[0] + endoff, v );
        offset_ = endoff;
    }

    void commit()
    {
        std::fill( &(*new_)[0] + offset_, &(*new_)[0] + S::size, rrs::zero( old_ ) );
        using std::swap;
        swap( old_, *new_ );
    }
private:
    S & old_;
    shared_ptr< S > new_;
    ptrdiff_t offset_;
};

As an extra nicety, the above inserter object is cheap to copy since it holds its new data in a shared_ptr<>. This can sometimes be useful in case the inserter ever gets used as an STL output iterator by higher-level wrappers.

Now that we have an appropriately defined inserter object, we need to "wire it up" to the OrderedInserter and Mutable_RangeRunStorage customization points. First, we define a tag type, like we did for fixed_storage<>:

struct fixed_storage_inserter_tag;

namespace boost { namespace sequence { namespace impl
{
    template< class S >
    struct tag< fixed_storage_inserter< S > >
    {
        typedef fixed_storage_inserter_tag type;
    };
}}}

Then we implement the OrderedInserter customization points, as follows:

namespace boost { namespace range_run_storage { namespace impl
{
    template< class S, class R, class V >
    struct set_at< S, R, V, fixed_storage_inserter_tag >
    {
        typedef void result_type;
        void operator()( S & s, R & r, V & v ) const
        {
            s.set_at( r, v );
        }
    };

    template< class S >
    struct commit< S, fixed_storage_inserter_tag >
    {
        typedef void result_type;
        void operator ()( S & s ) const
        {
            s.commit();
        }
    };
}}}

Finally, we hook the ordered_inserter() customization point to return one of our fixed_storage_inserter<> objects:

namespace boost { namespace range_run_storage { namespace impl
{
    template< class S >
    struct ordered_inserter< S, fixed_storage_tag >
    {
        typedef fixed_storage_inserter< S > result_type;
        result_type operator()( S & s ) const
        {
            return result_type( s );
        }
    };
}}}

Now, our fixed_storage<> type models Mutable_RangeRunStorage. To make it model Mutable_InfiniteRangeRunStorage, we need to implement a way to mutate the values of the pre- and post-run. For storage types like fixed_storage<> that do not have pre- and post-runs, we don't need to do anything. If we did have pre- and post-runs, we could make them mutable as follows:

namespace boost { namespace range_run_storage { namespace impl
{
    template< class S, class V >
    struct set_pre_value< S, V, fixed_storage_tag >
    {
        typedef void result_type;
        
        void operator()( S & s, V & v ) const
        {
            the-value-of-the-pre-run = v;
        }
    };
}}}

We would do the same for the post-run.

Now, our fixed_storage<> type is a full-fledged model of the Mutable_InfiniteRangeRunStorage concept, and we can do things like this:

// define a delta series
delta_series< int > delta( start = 5, value = 42 );

// define an empty fixed_storage
fixed_storage< int, 16 > fixed;

// make an ordered inserter for the fixed_storage
fixed_storage_inserter< fixed_storage< int, 16 > >
    out = range_run_storage::ordered_inserter( fixed );

// copy the delta_series into the fixed_storage inserter
range_run_storage::copy( delta, out );

// commit the changes to the fixed_storage
range_run_storage::commit( out );

// OK! The fixed_storage will be 0 everywhere except at offset 5
BOOST_ASSERT( 42 == fixed[ 5 ] );

Using time_series_facade<>

Our fixed_storage<> type models Mutable_InfiniteRangeRunStorage, but it does not yet satisfy the TimeSeries concept because it doesn't have a discretization. It would be a simple matter to define a wrapper that uses fixed_storage<> as a backing store and added a discretization() member function and indexed access. The Time_series library already provides such a wrapper that does that and much more, like:

  • Providing an intuitive named-parameter construction syntax,
  • Allowing conversion from any other TimeSeries type,
  • Defining Boolean series comparison operators, and
  • Defining a std::ostream insertion operator.

Using time_series_facade<> is fairly straight-forward. First, we define a type fixed_series<> that inherits from time_series_facade<>:

template< class T, size_t N, class Disc = int >
struct fixed_series
  : time_series_facade< fixed_series< T, N, Disc >, fixed_storage< T, N >, Disc >
{
    typedef
        time_series_facade< fixed_series< T, N, Disc >, fixed_storage< T, N >, Disc >
    base_type;
    
    // Use operator= from time_series_facade, which allows all models
    // of TimeSeries to be assigned to a fixed_series<>
    using base_type::operator=;
    
    // Define time series constructors that use named parameters
    BOOST_TIME_SERIES_DEFINE_CTORS( fixed_series )
};

Here, we use the BOOST_TIME_SERIES_DEFINE_CTORS macro to generate the fixed_series<> constructors. Already, we can declare a fixed_series<> object as: fixed_series< double, 64 > fs; and the result is a valid TimeSeries. Before we can use named parameters to specify a discretization, we have to teach time_series_facade<> what the named parameters for our fixed_series<> are. For that, we use a little utility in the boost::constructors namespace:

namespace boost { namespace constructors { namespace impl
{
    struct fixed_series_tag;

    template< class T, size_t N, class Disc >
    struct tag< fixed_series< T, N, Disc > >
    {
        typedef fixed_series_tag type;
    };

    template< class T >
    struct construct< T, fixed_series_tag >
      : arg_pack_construct
    {
        // OK, for fixed_series<>, the only allowable constructor
        // parameter is the discretization, and it is optional.
        typedef parameter::parameters<
            parameter::optional< time_series::tag::discretization >
        > args_type;
    };
}}}

Now we can say the following:

// A fixed-size series with a discretization of 30
fixed_series< double, 64 > fs1( time_series::discretization = 30 );

// Same as above
fixed_series< double, 64 > fs2( 30 );

And with that, we're finally done implementing fixed_series<>. Whew!

Copyright © 2006 Eric Niebler

PrevUpHomeNext